Есть ли проблемы с производительностью при использовании Pragma Pack(1)?

Наши заголовки используют #pragma pack(1) вокруг большинства наших структур (используется для сетевого и файлового ввода-вывода). Я понимаю, что это меняет выравнивание структур со значения по умолчанию 8 байтов до выравнивания 1 байта.

Предполагая, что все работает в 32-битном Linux (возможно, также и в Windows), есть ли какой-нибудь удар по производительности, связанный с этим выравниванием пакетов?

Меня не беспокоит переносимость библиотек, но больше касается совместимости файлового и сетевого ввода-вывода с различными пакетами #pragma и проблем с производительностью.

9 ответов

Решение

Доступ к памяти является самым быстрым, когда он может осуществляться по согласованным по словам адресам памяти. Простейшим примером является следующая структура (которую также использовал @Didier):

struct sample {
   char a;
   int b;
};

По умолчанию GCC вставляет заполнение, поэтому a имеет смещение 0, а b имеет смещение 4 (выравнивание по слову). Без заполнения b не выравнивается по словам, а доступ медленнее.

Насколько медленнее?

  • Для 32-разрядных x86, в соответствии с Руководством разработчика программного обеспечения для архитектуры Intel 64 и IA32:
    Процессору требуется два доступа к памяти, чтобы получить доступ к памяти без выравнивания; выровненный доступ требует только одного доступа к памяти. Операнд из слова или двойного слова, пересекающий 4-байтовую границу, или операнд из четырех слов, пересекающий 8-байтовую границу, считается невыровненным и требует двух отдельных циклов шины памяти для доступа.
    Как и в случае с большинством вопросов о производительности, вам нужно сравнить приложение с целью выяснить, насколько серьезна проблема на практике.
  • Согласно Википедии, расширения x86, такие как SSE2, требуют выравнивания слов.
  • Многие другие архитектуры требуют выравнивания слов (и будут генерировать ошибки SIGBUS, если структуры данных не выровнены по словам).

Что касается портативности: я предполагаю, что вы используете #pragma pack(1) так что вы можете отправлять структуры по проводам и с диска, не беспокоясь о разных компиляторах или платформах, по-разному упаковывающих структуры. Это верно, однако, есть несколько моментов, о которых следует помнить:

  • Это ничего не делает для решения проблем с прямым и обратным порядком байтов. Вы можете справиться с этим, вызвав семейство функций htons для любых целых, без знака и т. Д. В своих структурах.
  • По моему опыту, работа с упакованными, сериализуемыми структурами в коде приложения не доставляет большого удовольствия. Их очень сложно модифицировать и расширять, не нарушая обратной совместимости, и, как уже отмечалось, производительность снижается. Подумайте о том, чтобы перенести содержимое ваших упакованных сериализуемых структур в эквивалентные неупакованные, расширяемые структуры для обработки, или подумайте об использовании полноценной библиотеки сериализации, такой как Protocol Buffers (которая имеет привязки C).

Да. Там абсолютно есть.

Например, если вы определяете структуру:

struct dumb {
    char c;
    int  i;
};

затем всякий раз, когда вы обращаетесь к члену i, ЦП замедляется, потому что 32-битное значение i не доступно собственным, выровненным способом. Для простоты представьте, что ЦП должен получить 3 байта из памяти, а затем еще 1 байт из следующего местоположения, чтобы передать значение из памяти в регистры ЦП.

Когда вы объявляете структуру, большинство компиляторов вставляют байты заполнения между членами, чтобы гарантировать, что они выровнены по соответствующим адресам в памяти (обычно байты заполнения кратны размеру типа). Это позволяет компилятору оптимизировать доступ к этим элементам.

#pragma pack(1) указывает компилятору упаковать элементы структуры с определенным выравниванием. 1 здесь указывается компилятору не вставлять какие-либо отступы между членами.

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

В идеале лучше избегать изменения правил естественного выравнивания по умолчанию. Но если директивы "Прагма-упаковка" вообще нельзя избежать (как в вашем случае), то оригинальную схему упаковки следует восстановить после определения конструкций, которые требуют плотной упаковки.

Например:

//push current alignment rules to internal stack and force 1-byte alignment boundary
#pragma pack(push,1)  

/*   definition of structures that require tight packing go in here   */

//restore original alignment rules from stack    
#pragma pack(pop)

Это зависит от базовой архитектуры и способа обработки невыровненных адресов.

x86 корректно обрабатывает невыровненные адреса, хотя и с затратами на производительность, в то время как другие архитектуры, такие как ARM, могут вызывать ошибку выравнивания (SIGBUS) или даже "округлите" неверно выровненный адрес до ближайшей границы, и в этом случае ваш код будет сбои.

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

Есть ли проблемы с производительностью при использовании pragma pack(1)?

Абсолютно. В январе 2020 года Раймонд Чен из Microsoft опубликовал конкретные примеры использования #pragma pack(1)может создавать раздутые исполняемые файлы, которые требуют намного больше инструкций для выполнения операций с упакованными структурами. Особенно на оборудовании, отличном от x86, которое напрямую не поддерживает несогласованный доступ на оборудовании.

Любой, кто пишет #pragma pack(1) с таким же успехом могут просто носить на лбу табличку с надписью "Я ненавижу RISC".

Когда вы используете #pragma pack(1), это изменяет упаковку структуры по умолчанию на упаковку байтов, удаляя все байты заполнения, обычно вставляемые для сохранения выравнивания.

...

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

void UpdateS(S* s)
{
 s->total = s->a + s->b;
}

void UpdateP(P* p)
{
 p->total = p->a + p->b;
}

Несмотря на то, что структуры S и P имеют одинаковую компоновку, генерация кода отличается из-за выравнивания.

UpdateS                       UpdateP
Intel Itanium

adds  r31 = r32, 4            adds  r31 = r32, 4
adds  r30 = r32  8 ;;         adds  r30 = r32  8 ;;
ld4   r31 = [r31]             ld1   r29 = [r31], 1
ld4   r30 = [r30] ;;          ld1   r28 = [r30], 1 ;;
                              ld1   r27 = [r31], 1
                              ld1   r26 = [r30], 1 ;;
                              dep   r29 = r27, r29, 8, 8
                              dep   r28 = r26, r28, 8, 8
                              ld1   r25 = [r31], 1
                              ld1   r24 = [r30], 1 ;;
                              dep   r29 = r25, r29, 16, 8
                              dep   r28 = r24, r28, 16, 8
                              ld1   r27 = [r31]
                              ld1   r26 = [r30] ;;
                              dep   r29 = r27, r29, 24, 8
                              dep   r28 = r26, r28, 24, 8 ;;
add   r31 = r30, r31 ;;       add   r31 = r28, r29 ;;
st4   [r32] = r31             st1   [r32] = r31
                              adds  r30 = r32, 1
                              adds  r29 = r32, 2 
                              extr  r28 = r31, 8, 8
                              extr  r27 = r31, 16, 8 ;;
                              st1   [r30] = r28
                              st1   [r29] = r27, 1
                              extr  r26 = r31, 24, 8 ;;
                              st1   [r29] = r26
br.ret.sptk.many rp           br.ret.sptk.many.rp

...
[examples from other hardware]
...

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

Мораль истории: не применять #pragma pack(1)к конструкциям, кроме случаев крайней необходимости. Это раздувает ваш код и препятствует оптимизации.

#pragma pack(1)и его вариации также немного опасны - даже в системах x86, где они якобы "работают"

На некоторых платформах, таких как ARM Cortex-M0, 16-битные инструкции загрузки / сохранения не работают, если используются по нечетному адресу, а 32-битные инструкции не работают, если используются по адресам, не кратным четырем. Загрузка или сохранение 16-битного объекта с / на адрес, который может быть нечетным, потребует использования трех инструкций, а не одной; для 32-битного адреса потребуется семь инструкций.

В clang или gcc взятие адреса члена упакованной структуры даст указатель, который часто будет непригоден для доступа к этому члену. В более полезном компиляторе Keil, взяв адрес__packed член структуры даст __packedквалифицированный указатель, который может храниться только в объектах указателя, квалифицируемых аналогичным образом. Доступы, сделанные через такие указатели, будут использовать последовательность из нескольких инструкций, необходимую для поддержки невыровненного доступа.

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

Технически, да, это повлияет на производительность, но только в отношении внутренней обработки. Если вам нужны структуры, упакованные для ввода-вывода сети / файла, существует баланс между упакованным требованием и только внутренней обработкой. Под внутренней обработкой я подразумеваю работу, которую вы выполняете над данными между IO. Если вы выполняете очень мало обработки, вы не потеряете много с точки зрения производительности. В противном случае вы можете выполнить внутреннюю обработку на правильно выровненных структурах и только "упаковать" результаты при выполнении ввода-вывода. Или вы можете переключиться на использование только выровненных по умолчанию структур, но вам нужно убедиться, что все выровняли их одинаково (сетевые и файловые клиенты).

На современных процессорах x86 почти нет разницы в производительности, когда у вас есть невыровненный доступ. Единственная разница заключается в том, когда этот доступ пересекает границу страницы. Я пишу код, предназначенный только для процессоров x86. Поэтому, когда у меня есть большая структура данных с линейным доступом, где размеры объектов заметно выигрывают от#pragma pack(1), тем самым вызывая более плотную упаковку последовательных объектов, я используюpragma pack(1). Если бы я перенес этот код на платформы, не имеющие быстрого невыровненного доступа, я мог бы#ifdefэто прагмы. А иногда это относится и к структурам данных с произвольным доступом, в зависимости от их размера. Если они помещаются в кеш, эффект того, что слова занимают две строки кеша, и у вас есть еще одна загрузка строки кеша, может не иметь значения.

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