Есть ли проблемы с производительностью при использовании 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)
, это изменяет упаковку структуры по умолчанию на упаковку байтов, удаляя все байты заполнения, обычно вставляемые для сохранения выравнивания....
Возможность того, что любая структура 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
это прагмы. А иногда это относится и к структурам данных с произвольным доступом, в зависимости от их размера. Если они помещаются в кеш, эффект того, что слова занимают две строки кеша, и у вас есть еще одна загрузка строки кеша, может не иметь значения.