Недавно я видел кодовую базу, которая, как я боюсь, нарушает ограничения выравнивания. Я очистил его, чтобы создать минимальный пример, приведенный ниже. Кратко, игроки:
Бассейн. Это класс, который эффективно распределяет память для некоторого определения «эффективного». Пул гарантированно возвращает фрагмент памяти, выровненный по запрошенному размеру.
Объект_список. Этот класс хранит однородные наборы объектов. Как только количество объектов превышает определенный порог, он меняет свое внутреннее представление со списка на дерево. Размер Obj_list — один указатель (8 байт на 64-разрядной платформе). Его заполненный магазин, конечно, превысит это.
Совокупность. Этот класс представляет очень распространенный объект в системе. Его история восходит к ранней эре 32-разрядных рабочих станций, и в результате он был «оптимизирован» (в ту же 32-разрядную эпоху), чтобы использовать как можно меньше места. Агрегаты могут быть пустыми или управлять произвольным количеством объектов.
В этом примере элементы Agggregate всегда выделяются из пулов, поэтому они всегда выравниваются. Единственными вхождениями 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 стоила бы еще одного указателя в случае, когда Агрегированные объекты do имеют элементы.
Кроме того, я очень не хочу отвлекаться от главного вопроса, а именно от выравнивания. Я думаю, что вышеприведенная конструкция проблематична, но в стандарте я не могу найти ничего, кроме смутного обсуждения поведения выравнивания «системы/библиотеки» new.
Итак, делает ли приведенная выше конструкция что-то большее, чем случайные остановки трубы?
Изменить: я понимаю, что есть способы заменить подход с использованием встроенного массива символов. Так же поступали и оригинальные архитекторы. Они отказались от них, потому что память была в большом почете. Теперь, если у меня будет причина коснуться этого кода, я, вероятно, изменю его.
Однако я надеюсь, что люди ответят на мой вопрос о проблемах выравнивания, присущих этому подходу. Спасибо!