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

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

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

  • Obj_list. Этот класс хранит однородные коллекции объектов. Как только количество объектов превышает определенный порог, он меняет свое внутреннее представление со списка на дерево. Размер Obj_list - один указатель (8 байтов на 64-битной платформе). Его густонаселенный магазин, конечно, превзойдет это.

  • Совокупный. Этот класс представляет собой очень распространенный объект в системе. Его история восходит к ранней эре 32-битных рабочих станций, и в результате он был «оптимизирован» (в ту же 32-битную эпоху), чтобы использовать как можно меньше места. Совокупный может быть пустым или управлять произвольным количеством объектов.

В этом примере элементы Совокупный всегда выделяются из Бассейн, поэтому они всегда выровнены. Единственные вхождения Obj_list в этом примере - это «скрытые» элементы в объектах Совокупный, и поэтому они всегда выделяются с использованием размещение новое. Вот классы поддержки:

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;
}

У вас может возникнуть соблазн предложить заменить массив символов указателем на тип Obj_list, инициализированный значением NULL или экземпляром класса. Это дает правильную семантику, но просто меняет стоимость памяти. Если бы память все еще была в дефиците (а это могло быть, это представление базы данных EDA), замена массива char указателем на Obj_list потребовала бы еще одного указателя в случае, когда объекты Совокупныйделать имеют члены.

Кроме того, я не хочу отвлекаться от главного вопроса, а именно от выравнивания. Я считать вышеупомянутая конструкция проблематична, но я не могу найти больше в стандарте, чем какое-то расплывчатое обсуждение поведения выравнивания «системы / библиотеки» новый.

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

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

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

Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
2
0
576
5
Перейти к ответу Данный вопрос помечен как решенный

Ответы 5

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

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

- Агрегат выравнивается по пулу. - Агрегат происходит от Меньшего, что составляет двенадцать байтов. - member_list_store_d - массив символов в Aggregate. Вы хотите сказать, что компилятор выровняет этот элемент массива char по четырем байтам? Если да, то, предположительно, на 64-битной платформе он будет выровнен по восьми байтам?

Don Wakefield 29.10.2008 21:11

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

Обновлено: просто прочитайте OP еще раз - вы не хотите платить за другой указатель. Буду читать утром еще раз.

Думаю, я не понимаю. Массив символов member_list_store_d является переменной-членом Совокупный. Мы используем размещение new в в этом массиве символов для создания объекта Obj_list. Что вы предлагаете сделать с помощью malloc или глобального оператора new [] для этого существующего элемента данных массива?

Don Wakefield 30.10.2008 02:55

Как член данных, массив char не обязательно имеет какое-либо конкретное выравнивание. Вместо этого вы можете сделать member_list_store_d char * и выделить его с помощью malloc (sizeof (Obj_list)) или new char [sizeof (Obj_list)] в ctor агрегата. Это дает вам гарантию выравнивания с минимальными нарушениями.

fizzer 30.10.2008 03:04

Обратите внимание, что строго глобальный оператор new [] для char * гарантирует выравнивание только для типов, которые достаточно малы, чтобы поместиться в выделенный вами массив. Этого достаточно для всех практических целей (поэтому все это гарантировано). Но это действительно означает, что «new char [2]» не обязательно 4-выровнен.

Steve Jessop 30.10.2008 03:13

... в отличие от malloc (2). Предполагая платформу, где существует тип, требующий выравнивания по четырем точкам.

Steve Jessop 30.10.2008 04:39

У вас есть ссылка? Я смотрю на 3.7.3.1 «Возвращаемый указатель должен быть соответствующим образом выровнен, чтобы его можно было преобразовать в указатель любого полного типа объекта, а затем использовать для доступа к объекту или массиву в выделенном хранилище». Но это большой документ - я мог что-то упустить.

fizzer 30.10.2008 10:52

Может ли быть правило «как если» - соответствующая программа не может сказать, что она не согласована?

fizzer 30.10.2008 11:49

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

// MSVC
#pragma pack(push,1)

// structure definitions

#pragma pack(pop)

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

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

Это программное обеспечение работает в основном в Unix / Linux. Доступна ли эта прагма для компиляторов, предназначенных для * nix?

Don Wakefield 30.10.2008 03:45

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

class Aggregate: публичный Меньший { ... защищено: Obj_list list; };

Должно быть, я чего-то упускаю, но не могу понять, почему это плохо.

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

Как опубликовано в другом месте @ fizzer.myopen.com, этот подход был явно отвергнут первоначальными архитекторами, поскольку при создании объекта Obj_list потребовался бы дополнительный указатель.

Don Wakefield 30.10.2008 03:44

Как так? В вопросе говорилось, что «замена массива char указателем на Obj_list будет стоить еще один указатель в случае, когда у объектов Aggregate есть члены». Я говорю о пример Obj_list, а не об указателе.

user3458 30.10.2008 04:09

Извините, я неправильно понял. Таким образом, Obj_list (как реализовано) создаст объект коллекции во вторичном хранилище. Массив используется, чтобы избежать его построения, если объекты не предоставлены. Опять же, я считаю это решение разумным, но вопрос заключается в согласовании этой конструкции, а не в альтернативах.

Don Wakefield 30.10.2008 06:33

О, я думаю, мой ответ на вопрос о выравнивании довольно очевиден. Только не полагайся на это. И я пытаюсь показать, как избавиться от зависимости. Возможный конструктор для Obj_list установил бы pimpl в NULL и дождался первой вставки, прежде чем делать что-либо еще.

user3458 30.10.2008 16:32

@Arkadiy, спасибо за вклад. Я уже думал о заменах, включая ту, о которой вы только что упомянули. Эта тема предназначена только для обсуждения значения описанной «идиомы».

Don Wakefield 30.10.2008 17:26
Ответ принят как подходящий

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

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

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

    Obj_list list;
    

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

list.~Obj_list();

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

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

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

Меня это беспокоит с тех пор, как я наткнулся на него. Он присутствует в коде перевозки почти двадцать лет, но я, по-прежнему, чувствую желание изменить его на один из вариантов, которые вы (и я, и другие) перечислили. Есть какой-нибудь ответ на первое сообщение @ Andrew? Похоже, он подразумевает, что эта «идиома» «безопасна».

Don Wakefield 30.10.2008 17:32

Я не могу прочитать эту интерпретацию в его сообщении: он говорит, что это может быть проблемой на некоторых платформах. Если код проработал без происшествий 20 лет, ваша платформа явно не входит в их число. Неопределенное поведение просто не определено. Программа не терпит краха.

fizzer 30.10.2008 21:39

Другие вопросы по теме