Урок 23. Паттерн 15. Рост размеров структур

24.01.2012

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

Данные в структурах в языке Си++ выравниваются таким образом, чтобы обеспечить более эффективный к ним доступ. Некоторые микропроцессоры вообще не могут напрямую обращаться к не выровненным данным и компилятору приходиться генерировать специальный код для обращения к таким данным. Те же микропроцессоры, которые могут обращаться к не выровненным данным, все равно делают это намного менее эффективно. Поэтому компилятор Си++ оставляет пустые ячейки между полями структур, чтобы обеспечить их выравнивание по адресам машинных слов и тем самым ускорить к ним обращение. Можно отключить выравнивание, используя специальные директивы #pragma, чтобы сократить объем используемой памяти, но нас этот вариант сейчас не интересует. Часто можно значительно сократить объем расходуемой памяти простым изменением порядка полей в структуре, без потери производительности.

Рассмотрим следующую структуру:

struct MyStruct
{
  bool m_bool;
  char *m_pointer;
  int m_int;
};

На 32-битной системе эта структура займет 12 байт, и сократить этот размер не представляется возможным. Каждое поле выровнено по границе 4 байта. Даже если m_bool перенести в конец, это ничего не изменит. Компилятор все равно сделает размер структуры кратным 4 байтам для выравнивания таких структур в массивах.

В случае 64-битной сборки структура MyStruct займет уже 24 байта. Это понятно. В начале идет один байт под m_bool и 7 неиспользуемых байт для выравнивания, так как указатель занимает 8 байт и должен быть выровнен по границе 8 байт. Затем 4 байта для m_int и 4 неиспользуемых байта, для выравнивания структуры по границе 8 байт.

К счастью, дело можно легко поправить, переместив m_bool в конец структуры, как показано ниже:

struct MyStructOpt
{
  char *m_pointer;
  int m_int;
  bool m_bool;
};

Структура MyStructOpt займет уже не 24, а 16 байт. Визуально расположение полей представлено на рисунке 1. Весьма существенная экономия, если мы будем использовать, например, 10 миллионов элементов. В этом случае мы сэкономим 80 мегабайт памяти, но что еще более важно, можем повысить производительность. Если структур будет немного, то нет разницы, какого они размера. Доступ будет происходить с одинаковой скоростью. Но когда элементов много, то начинает играть роль кэш, количество обращений к памяти и так далее. И можно с уверенностью утверждать, что обработка 160 мегабайт данных займет меньше времени, чем 240 мегабайт. Даже простой доступ ко всем элементам массива для чтения, уже будет более быстр.

Рисунок 1 - Расположение полей в структурах MyStruct и MyStructOpt

Рисунок 1 - Расположение полей в структурах MyStruct и MyStructOpt

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

Вы, наверное, зададите вопрос, по каким правилам компилятор выравнивает данные. Мы ответим кратко, а если интересно познакомиться с этой темой более подробно, то мы отсылаем вас к книге Джеффри Рихтер - Создание эффективных WIN32-приложений с учетом специфики 64-разрядной версии Windows. Там этот вопрос рассматривается достаточно подробно.

В целом правило выравнивание следующее: каждое поле выравнивается по адресу, кратному размеру данного поля. Поле типа size_t на 64-битной системе будет выровнено по границе 8 байт, int по границе 4 байта, short по границе 2 байта. Поля типа char не выравниваются. Размер структуры выравнивается до размера, кратному размеру его максимального элемента. Поясним это выравнивание на примере:

struct ABCD
 {
  size_t m_a;
  char m_b;
 };

Элементы займут 8 + 1 = 9 байт. Но если размер структуры будет 9 байт, то, если мы захотим создать массив структур ABCD[2], поле m_a второй структуры будет лежать по не выровненному адресу. Вследствие этого компилятор дополнит структуру 7 пустыми байтами до размера 16 байт.

Может показаться сложным процесс оптимизации последовательности полей. Но можно предложить очень простой и очень эффективный способ. Достаточно расположить поля в порядке убывания их размера. Этого будет совершенно достаточно. В этом случае поля начнут располагаться без лишних зазоров. Например, возьмем следующую структуру размером 40 байт

struct MyStruct
{
  int m_int;
  size_t m_size_t;
  short m_short;
  void *m_ptr;
  char m_char;
};

и простой сортировкой последовательности полей по убыванию размера:

struct MyStructOpt
{
  void *m_ptr;
  size_t m_size_t;
  int m_int;
  short m_short;
  char m_char;
};

мы сделаем из нее структуру размером всего 24 байт.

Диагностика

Инструмент PVS-Studio позволяет обнаружить структуры в коде 64-битных приложений, перестановка полей в которых, позволит сократить их размер. На неоптимальные структуры анализатор выдает диагностическое сообщение V401.

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

class MyWindow : public CWnd {
  bool m_isActive;
  size_t m_sizeX, m_ sizeY;
  char m_color[3];
  ...
};

Размер данной структуры может быть сокращен, но это не имеет практического смысла.

Авторы курса: Андрей Карпов (karpov@viva64.com), Евгений Рыжков (evg@viva64.com).

Правообладателем курса "Уроки разработки 64-битных приложений на языке Си/Си++" является ООО "Системы программной верификации". Компания занимается разработкой программного обеспечения в области анализа исходного кода программ. Сайт компании: http://www.viva64.com.

Контактная информация: e-mail: support@viva64.com, 300027, г. Тула, а/я 1800.