Существуют ли проблемы с производительностью при использовании пакета pragma (1)?

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

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

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

Ответ 1

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

struct sample {
   char a;
   int b;
};

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

Сколько медленнее?

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

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

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

Ответ 2

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

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

struct dumb {
    char c;
    int  i;
};

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

Ответ 3

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

#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)

Ответ 4

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

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

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

Ответ 5

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

Ответ 6

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

Ответ 7

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

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