Урок 21. Паттерн 13. Выравнивание данных
Процессоры работают эффективнее, когда имеют дело с правильно выровненными данными. А некоторые процессоры вообще не умеют работать с не выровненными данными. Попытка работать с не выровненными данными на процессорах IA-64 (Itanium), как показано в следующем примере, приведет к возникновению исключения:
#pragma pack (1) // Also set by key /Zp in MSVC
struct AlignSample {
unsigned size;
void *pointer;
} object;
void foo(void *p) {
object.pointer = p; // Alignment fault
}
Если вы вынуждены работать с не выровненными данными на Itanium, то следует явно указать это компилятору. Например, воспользоваться специальным макросом UNALIGNED:
#pragma pack (1) // Also set by key /Zp in MSVC
struct AlignSample {
unsigned size;
void *pointer;
} object;
void foo(void *p) {
*(UNALIGNED void *)&object.pointer = p; //Very slow
}
В этом случае компилятор сгенерирует специальный код, который будет работать с не выровненными данными. Такое решение неэффективно, так как доступ к данным будет происходить в несколько раз медленнее. Если целью является уменьшение размера структуры, то лучшего результата можно достичь, располагая данные в порядке уменьшения их размера. Подробнее об этом будет рассказано в одном из следующих уроков.
На архитектуре x64 при обращении к не выровненным данным исключения не возникает, но их также следует избегать. Во-первых, из-за существенного замедления скорости доступа к таким данным, а во-вторых, из-за возможности переноса программы в будущем на платформу IA-64.
Рассмотрим еще один пример кода, не учитывающий выравнивание данных:
struct MyPointersArray {
DWORD m_n;
PVOID m_arr[1];
} object;
...
malloc( sizeof(DWORD) + 5 * sizeof(PVOID) );
...
Если мы хотим выделить объем памяти, необходимый для хранения объекта типа MyPointersArray, содержащего 5 указателей, то мы должны учесть, что начало массива m_arr будет выровнено по границе 8 байт. Расположение данных в памяти на разных системах (Win32/Win64) показано на рисунке 1.
Рисунок 1- Выравнивание данных в памяти на системах Win32 и Win64
Корректный расчет размера должен выглядеть следующим образом:
struct MyPointersArray {
DWORD m_n;
PVOID m_arr[1];
} object;
...
malloc( FIELD_OFFSET(struct MyPointersArray, m_arr) +
5 * sizeof(PVOID) );
...
В приведенном коде мы узнаем смещение последнего члена структуры и суммируем это смещение с его размером. Смещение члена структуры или класса можно узнать с использованием макроса offsetof или FIELD_OFFSET.
Всегда используйте эти макросы для получения смещения в структуре, не опираясь на знание размеров типов и выравнивания. Пример кода с правильным вычислением адреса члена структуры:
struct TFoo {
DWORD_PTR whatever;
int value;
} object;
int *valuePtr =
(int *)((size_t)(&object) + offsetof(TFoo, value)); // OK
Разработчиков Linux-приложений может ждать еще одна неприятность, связанная с выравниванием. О ней вы можете прочитать в нашем блоге в посте "Изменения выравнивания типов и последствия".
Диагностика
Поскольку работа с не выровненными данными не приводит к ошибке на архитектуре x64, а только к снижению производительности, инструмент PVS-Studio не предупреждает об упакованных структурах. Но если для вас критична производительность приложения, рекомендуем просмотреть все места в программе, где используется "#pragma pack". Для архитектуры IA-64 данная проверка более актуальна, но анализатор PVS-Studio пока не ориентирован на верификацию программ для IA-64. Если вы работаете с системами на базе Itanium и планируете приобрести PVS-Studio, напишите нам, и мы обсудим вопросы адаптации этого инструмента к особенностям IA-64.
Инструмент PVS-Studio позволяет обнаружить ошибки, связанные с вычислением размеров объектов и смещений. Анализатор обнаруживает опасные арифметические выражения, содержащие в себе несколько операторов sizeof(), что свидетельствует о возможной ошибке. Диагностическое сообщение имеет номер V119.
Однако во многих случаях использование нескольких операторов sizeof() в рамках одного выражения корректно и анализатор игнорирует подобные конструкции. Пример безопасных выражений с несколькими операторами sizeof:
int MyArray[] = { 1, 2, 3 };
size_t MyArraySize =
sizeof(MyArray) / sizeof(MyArray[0]); //OK
assert(sizeof(unsigned) < sizeof(size_t)); //OK
size_t strLen = sizeof(String) - sizeof(TCHAR); //OK
Приложение
На рисунке 2 представлены размеры типов и их выравнивание. Для изучения размеров объектов и их выравнивания на различных платформах вы также можете воспользоваться примером кода, приведенным в записи блога "Изменения выравнивания типов и последствия".
Рисунок 2 - Размеры типов и их выравнивание.
Авторы курса: Андрей Карпов (karpov@viva64.com), Евгений Рыжков (evg@viva64.com).
Правообладателем курса "Уроки разработки 64-битных приложений на языке Си/Си++" является ООО "Системы программной верификации". Компания занимается разработкой программного обеспечения в области анализа исходного кода программ. Сайт компании: http://www.viva64.com.