Когда я должен беспокоиться о выравнивании?
Недавно я немного узнал о выравнивании, но я не уверен, в каких ситуациях это будет проблемой или нет. Есть два случая, которые меня интересуют:
Первый - при использовании массивов:
struct Foo {
char data[3]; // size is 3, my arch is 64-bit (8 bytes)
};
Foo array[4]; // total memory is 3 * 4 = 12 bytes.
// will this be padded to 16?
void testArray() {
Foo foo1 = array[0];
Foo foo2 = array[1]; // is foo2 pointing to a non-aligned location?
// should one expect issues here?
}
Второй случай - при использовании пула памяти:
struct Pool {
Pool(std::size_t size = 256) : data(size), used(0), freed(0) { }
template<class T>
T * allocate() {
T * result = reinterpret_cast<T*>(&data[used]);
used += sizeof(T);
return result;
}
template<class T>
void deallocate(T * ptr) {
freed += sizeof(T);
if (freed == used) {
used = freed = 0;
}
}
std::vector<char> data;
std::size_t used;
std::size_t freed;
};
void testPool() {
Pool pool;
Foo * foo1 = pool.allocate<Foo>(); // points to data[0]
Foo * foo2 = pool.allocate<Foo>(); // points to data[3],
// alignment issue here?
pool.deallocate(foo2);
pool.deallocate(foo1);
}
Мои вопросы:
- Есть ли проблемы с выравниванием в двух примерах кода?
- Если да, то как их можно исправить?
- Где я могу узнать больше об этом?
Обновить
Я использую 64-битный процессор Intel i7 с Darwin GCC. Но я также использую Linux, Windows (VC2008) для 32-битных и 64-битных систем.
Обновление 2
Пул теперь использует вектор вместо массива.
4 ответа
struct Foo {
char data[3]; // size is 3, my arch is 64-bit (8 bytes)
};
[Правка: я должен был быть более явным: заполнение разрешено здесь, в структуре после data
член (но не до этого).
Foo array[4]; // total memory is 3 * 4 = 12 bytes.
Заполнение здесь не допускается. Массивы должны быть смежными.
[edit: но между структурами в массиве не допускается заполнение - один struct
в массиве должны следовать сразу за другим - но, как отмечалось выше, каждая структура может сама содержать отступы.]
void testArray() {
Foo * foo1 = array[0];
Foo * foo2 = array[1]; // is foo2 pointing to a non-aligned location?
// should I expect issues here?
}
Опять же, прекрасно - компилятор должен это разрешить 1.
Для вашего пула памяти прогноз не так хорош, хотя. Вы выделили массив char
, который должен быть достаточно выровнен, чтобы char
, но доступ к нему как к любому другому типу не гарантированно работает. Реализация не может накладывать какие-либо ограничения на доступ к данным, как char
в любом случае, хотя.
Как правило, для такой ситуации вы создаете объединение всех типов, которые вам нужны, и выделяете их массив. Это гарантирует, что данные выровнены для использования в качестве объекта любого типа в объединении.
Кроме того, вы можете выделить свой блок динамически - оба malloc
а также operator ::new
гарантировать, что любой блок памяти выровнен для использования в качестве любого типа.
Изменить: изменение пула для использования vector<char>
улучшает ситуацию, но ненамного. Это означает, что первый выделенный вами объект будет работать, потому что блок памяти, содержащийся в векторе, будет выделен (косвенно) с помощью operator ::new
(так как вы не указали иначе). К сожалению, это не очень помогает - второе распределение может быть полностью смещено.
Например, предположим, что для каждого типа требуется "естественное" выравнивание, т. Е. Выравнивание по границе, равной его собственному размеру. Символ может быть выделен по любому адресу. Предположим, что short равен 2 байта и требует четного адреса, а int и long 4 байта и требуют выравнивания 4 байтов.
В этом случае подумайте, что произойдет, если вы сделаете:
char *a = Foo.Allocate<char>();
long *b = Foo.Allocate<long>();
Блок, с которого мы начали, должен был быть выровнен для любого типа, так что это был определенно четный адрес. Когда мы выделяем char
Мы используем только один байт, поэтому следующий доступный адрес нечетный. Затем мы выделяем достаточно места для long
, но он по нечетному адресу, поэтому попытка разыменования дает UB.
1 В основном в любом случае - в конечном счете, компилятор может отклонить практически что угодно под видом превышения лимита реализации. Я был бы удивлен, увидев, что настоящий компилятор имеет проблемы с этим, хотя.
Никто еще не упомянул пул памяти. Это имеет огромные проблемы с выравниванием.
T * result = reinterpret_cast<T*>(&data[used]);
Это не хорошо. Когда вы берете на себя управление памятью, вам нужно взять на себя все аспекты управления памятью, а не только распределение. Возможно, вы выделили правильный объем памяти, но вы вообще не обращались к выравниванию.
Предположим, вы используете new
или же malloc
выделить один байт. Распечатайте его адрес. Сделайте это снова и напечатайте этот новый адрес:
char * addr1 = new char;
std::cout << "Address #1 = " << (void*) addr1 << "\n";
char * addr2 = new char;
std::cout << "Address #2 = " << (void*) addr2 << "\n";
На 64-битном компьютере, таком как ваш Mac, вы увидите, что оба напечатанных адреса заканчиваются нулем, и они обычно находятся на расстоянии 16 байтов. Вы не выделили два байта здесь. Вы выделили 32! Это потому что malloc
всегда возвращает указатель, который выровнен так, что его можно использовать для любого типа данных.
Поместите двойное или длинное длинное int на адрес, который не заканчивается на 8 или 0 при печати в шестнадцатеричном формате, и вы, скорее всего, получите дамп памяти. Двойные и длинные длинные целые должны быть выровнены по 8-байтовым границам. Аналогичные ограничения применяются к простым старым ванильным целым числам (int32_t); они должны быть выровнены по 4-байтовым границам. Ваш пул памяти не делает этого.
Выравнивание обрабатывается компилятором прозрачно - sizeof и доступ к массиву всегда учитывают любое выравнивание, и вам не нужно об этом заботиться.
Однако в примере пула памяти есть ошибка - если вы вызываете deallocate(), он всегда освобождает последний выделенный указатель вместо заданного указателя.
Как правило, то есть для большинства структур данных, заранее не беспокойтесь о выравнивании. Компилятор обычно делает правильные вещи. Дни штрафных санкций за невыровненные данные прошли как минимум за 20 лет.
Единственными оставшимися проблемами являются нелегальный доступ к данным без выравнивания, который происходит только в меньшинстве архитектур ЦП. Напишите код, чтобы он имел смысл. Попробуй это. Если возникает исключение невыровненных данных, то пришло время выяснить, как его избежать. Большинство случаев легко исправить, добавив параметр командной строки. Некоторые требуют изменения структуры: изменение порядка элементов или явная вставка неиспользуемых элементов заполнения.