Какова производительность, безопасность и выравнивание элемента данных, скрытого во встроенном массиве символов в классе С++?

Недавно я видел кодовую базу, которая, как я боюсь, нарушает ограничения выравнивания. Я очистил его, чтобы создать минимальный пример, приведенный ниже. Кратко, игроки:

  • Бассейн. Это класс, который эффективно распределяет память для некоторого определения «эффективного». Пул гарантированно возвращает фрагмент памяти, выровненный по запрошенному размеру.

  • Объект_список. Этот класс хранит однородные наборы объектов. Как только количество объектов превышает определенный порог, он меняет свое внутреннее представление со списка на дерево. Размер 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.

Итак, делает ли приведенная выше конструкция что-то большее, чем случайные остановки трубы?

Изменить: я понимаю, что есть способы заменить подход с использованием встроенного массива символов. Так же поступали и оригинальные архитекторы. Они отказались от них, потому что память была в большом почете. Теперь, если у меня будет причина коснуться этого кода, я, вероятно, изменю его.

Однако я надеюсь, что люди ответят на мой вопрос о проблемах выравнивания, присущих этому подходу. Спасибо!


person Don Wakefield    schedule 29.10.2008    source источник


Ответы (5)


Хорошо - имел возможность прочитать это должным образом. У вас есть проблема с выравниванием, и вы вызываете неопределенное поведение при доступе к массиву символов как к Obj_list. Скорее всего, ваша платформа сделает одну из трех вещей: позволит вам сойти с рук, позволит вам избежать наказания за время выполнения или время от времени вылетит из-за ошибки шины.

Ваши портативные варианты, чтобы исправить это:

  • выделить хранилище с помощью malloc или глобальной функции выделения, но вы думаете, что это слишком дорого.
  • как говорит Аркадий, сделайте свой буфер членом Obj_list:

    Obj_list list;
    

но теперь вы не хотите оплачивать стоимость строительства. Вы можете смягчить это, предоставив встроенный конструктор, который будет использоваться только для создания этого экземпляра - как это сделал бы конструктор по умолчанию. Если вы следуете этому маршруту, настоятельно рассмотрите возможность вызова dtor

list.~Obj_list();

прежде чем делать размещение нового в этом хранилище.

В противном случае, я думаю, у вас останутся непереносимые варианты: либо полагайтесь на устойчивость вашей платформы к смещенному доступу, либо используйте любые непереносимые параметры, которые дает вам ваш компилятор.

Отказ от ответственности: вполне возможно, что мне не хватает трюка с союзами или чем-то подобным. Это необычная проблема.

person fizzer    schedule 30.10.2008
comment
Меня это раздражало с тех пор, как я наткнулся на него. Он используется в коде доставки почти двадцать лет, но я до сих пор испытываю желание изменить его для одного из вариантов, которые вы (и я, и другие) перечислили. . Есть ответ на первый пост @Andrew? Кажется, он подразумевает, что эта «идиома» «безопасна». - person Don Wakefield; 30.10.2008
comment
Я не могу прочитать эту интерпретацию в его посте: он говорит, что это может быть проблемой на некоторых платформах. Если код работает 20 лет без происшествий, ваша платформа явно не входит в их число. Неопределенное поведение просто не определено. Программа не обязана аварийно завершать работу. - person fizzer; 30.10.2008

Выравнивание будет выбрано компилятором в соответствии с его значениями по умолчанию, вероятно, в GCC/MSVC это будет четыре байта.

Это должно быть проблемой, только если есть код (SIMD/DMA), который требует определенного выравнивания. В этом случае вы должны иметь возможность использовать директивы компилятора, чтобы убедиться, что member_list_store_d выровнен, или увеличить размер на (выравнивание-1) и использовать соответствующее смещение.

person Andrew Grant    schedule 29.10.2008
comment
- Агрегат выравнивается по пулу. - Aggregate происходит от Lesser, что составляет двенадцать байтов. - member_list_store_d — это массив символов в Aggregate. Вы говорите, что компилятор выровняет этот член массива символов по четырем байтам? Если да, то, по-видимому, на 64-битной платформе он будет выравниваться по восьми байтам? - person Don Wakefield; 29.10.2008

Можете ли вы просто иметь экземпляр Obj_list внутри Aggregate? IOW, что-то вроде

class Aggregate: public Lesser { ... protected: Obj_list list; };

Я должен что-то упустить, но я не могу понять, почему это плохо.

Что касается вашего вопроса - он полностью зависит от компилятора. Однако большинство компиляторов по умолчанию выравнивают каждый член по границе слова, даже если тип члена не нужно выравнивать таким образом для правильного доступа.

person Community    schedule 30.10.2008
comment
Как писал @fizzer.myopen.com в другом месте, этот подход, по-видимому, был отвергнут первоначальными архитекторами, потому что он стоил бы дополнительного указателя при построении объекта Obj_list. - person Don Wakefield; 30.10.2008
comment
Как же так? В вопросе говорилось, что замена массива char указателем на Obj_list будет стоить еще одного указателя в случае, когда у объектов Aggregate есть члены. Я говорю об экземпляре Obj_list, а не об указателе. - person ; 30.10.2008
comment
Извините, я неправильно понял. Таким образом, Obj_list (как реализовано) создаст объект коллекции во вторичном хранилище. Массив используется, чтобы избежать его создания, если объекты не предоставлены. Опять же, я нахожу это решение разумным, но вопрос в согласовании этой конструкции, а не в альтернативах. - person Don Wakefield; 30.10.2008
comment
О, мой ответ на проблему выравнивания, я думаю, вполне очевиден. Просто не полагайтесь на это. И я пытаюсь показать, как избавиться от зависимости. Возможный конструктор для Obj_list установил бы pimpl в NULL и дождался бы первой вставки, прежде чем делать что-либо еще. - person ; 30.10.2008
comment
@Аркадий, спасибо за участие. Я уже думал о замене, включая ту, которую вы только что упомянули. Эта тема предназначена только для обсуждения последствий описанной «идиомы». - person Don Wakefield; 30.10.2008

Если вы хотите обеспечить выравнивание своих структур, просто выполните

// MSVC
#pragma pack(push,1)

// structure definitions

#pragma pack(pop)

// *nix
struct YourStruct
{
    ....
} __attribute__((packed));

Чтобы обеспечить выравнивание 1 байта вашего массива символов в Aggregate

person PiNoYBoY82    schedule 29.10.2008
comment
Это программное обеспечение работает в основном на Unix/Linux. Доступна ли эта прагма для компиляторов, предназначенных для *nix? - person Don Wakefield; 30.10.2008

Выделите массив символов member_list_store_d с помощью malloc или глобального оператора new[], любой из которых даст хранилище, выровненное для любого типа.

Редактировать: просто прочитайте OP еще раз - вы не хотите платить за еще один указатель. Утром снова прочитаю.

person fizzer    schedule 29.10.2008
comment
Кажется, я не понимаю. Массив символов member_list_store_d является переменной-членом Aggregate. Мы используем размещение new в в этом массиве символов для создания объекта Obj_list. Что вы предлагаете мне делать с malloc или глобальным оператором new[] для этого существующего элемента данных массива? - person Don Wakefield; 30.10.2008
comment
В качестве члена данных массив символов не обязательно имеет какое-либо конкретное выравнивание. Вместо этого вы можете сделать member_list_store_d символом* и выделить его с помощью malloc(sizeof(Obj_list)) или new char[sizeof(Obj_list)] в ctor Aggregate. Это дает вам гарантию выравнивания с минимальными нарушениями. - person fizzer; 30.10.2008
comment
Обратите внимание, что строго глобальный оператор new[] для char* гарантирует выравнивание только для типов, которые достаточно малы, чтобы поместиться в выделенный вами массив. Этого достаточно для всех практических целей (именно поэтому это все, что гарантировано). Но это означает, что new char[2] не обязательно имеет 4-выравнивание. - person Steve Jessop; 30.10.2008
comment
... в отличие от malloc(2). Предполагая платформу, на которой существует тип, требующий 4-выравнивания. - person Steve Jessop; 30.10.2008
comment
У вас есть цитата? Я смотрю на 3.7.3.1 «Возвращенный указатель должен быть соответствующим образом выровнен, чтобы его можно было преобразовать в указатель любого полного типа объекта, а затем использовать для доступа к объекту или массиву в выделенном хранилище». Но это большой документ - я мог что-то упустить. - person fizzer; 30.10.2008
comment
Может быть правило "как если бы" - соответствующая программа не может сказать, что она не выровнена? - person fizzer; 30.10.2008