Что такое производительность, безопасность и выравнивание члена Data, скрытого во встроенном массиве символов в классе C++?
Недавно я видел кодовую базу, которая, боюсь, нарушает ограничения выравнивания. Я вычистил его, чтобы привести минимальный пример, приведенный ниже. Вкратце, игроками являются:
Бассейн Это класс, который эффективно распределяет память, для некоторого определения "работоспособный". Пул гарантированно возвращает кусок памяти, который выровнен для запрошенного размера.
Obj_list. Этот класс хранит однородные коллекции объектов. Как только число объектов превышает определенный порог, оно меняет свое внутреннее представление со списка на дерево. Размер Obj_list - один указатель (8 байт на 64-битной платформе). Его заполненный магазин конечно превысит это.
Агрегат Этот класс представляет собой очень распространенный объект в системе. Его история восходит к ранней эре 32-битных рабочих станций, и он был "оптимизирован" (в ту же 32-битную эру), чтобы в результате использовать как можно меньше места. Агрегаты могут быть пустыми или управлять произвольным числом объектов.
В этом примере агрегированные элементы всегда выделяются из пула, поэтому они всегда выровнены. Единственные вхождения Obj_list в этом примере - это "скрытые" элементы в объектах Aggregate, и поэтому они всегда размещаются с использованием размещения new. Вот классы поддержки:
class Pool
{
public:
Pool();
virtual ~Pool();
void *allocate(size_t size);
static Pool *default_pool(); // returns a global pool
};
class Obj_list
{
public:
inline void *operator new(size_t s, void * p) { return p; }
Obj_list(const Args *args);
// when constructed, Obj_list will allocate representation_p, which
// can take up much more space.
~Obj_list();
private:
Obj_list_store *representation_p;
};
А вот и агрегат. Обратите внимание, что объявление члена member_list_store_d:
// Aggregate is derived from Lesser, which is twelve bytes in size
class Aggregate : public Lesser
{
public:
inline void *operator new(size_t s) {
return Pool::default_pool->allocate(s);
}
inline void *operator new(size_t s, Pool *h) {
return h->allocate(s);
}
public:
Aggregate(const Args *args = NULL);
virtual ~Aggregate() {};
inline const Obj_list *member_list_store_p() const;
protected:
char member_list_store_d[sizeof(Obj_list)];
};
Именно этот член данных меня больше всего беспокоит. Вот псевдокод для инициализации и доступа:
Aggregate::Aggregate(const Args *args)
{
if (args) {
new (static_cast<void *>(member_list_store_d)) Obj_list(args);
}
else {
zero_out(member_list_store_d);
}
}
inline const Obj_list *Aggregate::member_list_store_p() const
{
return initialized(member_list_store_d) ? (Obj_list *) &member_list_store_d : 0;
}
У вас может возникнуть желание предложить заменить массив char указателем на тип Obj_list, инициализированный значением NULL или экземпляром класса. Это дает правильную семантику, но просто меняет стоимость памяти. Если бы память все еще была дороже (а это может быть представление базы данных EDA), замена массива char указателем на список Obj_list стоила бы еще один указатель в случае, когда объекты Aggregate действительно имеют членов.
Кроме того, я не хочу отвлекаться от основного вопроса, который здесь касается выравнивания. Я думаю, что вышеупомянутая конструкция проблематична, но на самом деле не может найти больше в стандарте, чем какое-то смутное обсуждение поведения выравнивания в 'system / library' new.
Итак, делает ли приведенная выше конструкция что-то большее, чем случайная остановка трубы?
Изменить: я понимаю, что есть способы заменить подход, используя встроенный массив символов. То же самое сделали оригинальные архитекторы. Они отказались от них, потому что память была на высоте. Теперь, если у меня есть причина прикоснуться к этому коду, я, вероятно, изменю его.
Однако мой вопрос о проблемах выравнивания, присущих этому подходу, - это то, на что, я надеюсь, люди обратятся. Спасибо!
5 ответов
Хорошо - был шанс прочитать это правильно. У вас есть проблема с выравниванием, и вы вызываете неопределенное поведение, когда вы получаете доступ к массиву char как Obj_list. Скорее всего, ваша платформа выполнит одно из трех действий: пусть вам это сойдет с рук, пусть это сойдет вам с рук при штрафных санкциях во время выполнения или изредка вылетает из-за ошибки шины.
Ваши портативные варианты, чтобы исправить это:
- Выделите хранилище с помощью malloc или глобальной функции выделения, но вы думаете, что это слишком дорого.
как говорит Аркадий, сделайте ваш буфер членом Obj_list:
Obj_list list;
но вы сейчас не хотите оплачивать стоимость строительства. Вы могли бы смягчить это, предоставив встроенный неиспользуемый конструктор, который будет использоваться только для создания этого экземпляра - как это сделал бы конструктор по умолчанию. Если вы следуете по этому пути, настоятельно рекомендуем вызвать dtor
list.~Obj_list();
перед размещением нового в этом хранилище.
В противном случае, я думаю, что у вас остались непереносимые параметры: либо полагайтесь на допущение не выровненных обращений вашей платформы, либо используйте любые непереносимые параметры, предоставляемые вашим компилятором.
Отказ от ответственности: вполне возможно, я пропускаю трюк с профсоюзами или что-то подобное. Это необычная проблема.
Выравнивание будет выбрано компилятором в соответствии с его значениями по умолчанию, вероятно, в GCC / MSVC это будет четыре байта.
Это должно быть проблемой, только если есть код (SIMD/DMA), который требует определенного выравнивания. В этом случае вы должны быть в состоянии использовать директивы компилятора, чтобы гарантировать выравнивание member_list_store_d, или увеличить размер на (alignment-1) и использовать соответствующее смещение.
Если вы хотите обеспечить выравнивание ваших структур, просто сделайте
// MSVC
#pragma pack(push,1)
// structure definitions
#pragma pack(pop)
// *nix
struct YourStruct
{
....
} __attribute__((packed));
Чтобы обеспечить 1-байтовое выравнивание вашего массива символов в Aggregate
Можете ли вы просто иметь экземпляр Obj_list внутри Aggregate? IOW, что-то вроде
класс Aggregate: public Lesser { ... protected: список Obj_list; };
Должно быть, я что-то упускаю, но не могу понять, почему это плохо.
Что касается вашего вопроса - он полностью зависит от компилятора. Большинство компиляторов, тем не менее, по умолчанию выравнивают каждый элемент на границе слова, даже если для правильного доступа тип элемента не нужно выравнивать таким образом.
Выделить массив символов member_list_store_d
с malloc или глобальным оператором new[], любой из которых даст выравнивание памяти для любого типа.
Изменить: Просто прочитайте ОП снова - вы не хотите платить за другой указатель. Будем читать снова утром.