Подтвердить что ты не робот

Когда я должен беспокоиться о выравнивании?

Недавно я немного узнал о выравнивании, но я не уверен, в каких ситуациях это будет проблемой или нет. Меня интересуют два случая:

Первый - при использовании массивов:

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);
}

Мои вопросы:

  • Есть ли проблемы с выравниванием в двух примерах кода?
  • Если да, то как они могут быть исправлены?
  • Где я могу узнать больше об этом?

Update

Я использую 64-разрядный процессор Intel i7 с Darwin GCC. Но я также использую Linux, Windows (VC2008) для 32-разрядных и 64-разрядных систем.

Обновление 2

В пуле теперь используется вектор вместо массива.

4b9b3361

Ответ 1

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 (так как вы не указали иначе). К сожалению, это не очень помогает - второе выделение может быть полностью смещено.

Например, предположим, что для каждого типа требуется "естественное" выравнивание, т.е. выравнивание с границей, равной ее собственному размеру. A char может быть выделен по любому адресу. Мы предположим, что короткий - это 2 байта и требует четного адреса, а int и long - 4 байта и требуют 4-байтового выравнивания.

В этом случае подумайте, что произойдет, если вы выполните:

char *a = Foo.Allocate<char>();
long *b = Foo.Allocate<long>();

Блок, с которого мы начали, должен был быть выровнен для любого типа, поэтому он определенно был четным адресом. Когда мы выделяем char, мы используем только один байт, поэтому следующий доступный адрес является нечетным. Затем мы выделяем достаточно места для long, но на нечетном адресе, поэтому попытка разыменования дает UB.


1 В любом случае, в конечном счете, компилятор может отказаться от всего, что под видом превышения предела реализации. Я был бы удивлен, увидев, что у реального компилятора есть проблема с этим.

Ответ 2

Никто еще не упомянул о пуле памяти. Это имеет огромные проблемы с выравниванием.

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 байтовых границах. Ваш пул памяти не делает этого.

Ответ 3

Обычно для большинства структур данных - не беспокойтесь о настройке заранее. Компилятор, как правило, поступает правильно. Дни штрафов за потение за неудовлетворенные данные по меньшей мере на 20 лет отстают от нас.

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

Ответ 4

Выравнивание обрабатывается прозрачно компилятором - sizeof и обращения к массиву всегда учитывают любое выравнивание, и вам не нужно заботиться об этом.

Там есть ошибка в примере пула памяти - если вы вызываете deallocate(), он всегда освобождает выделенный выделенный указатель вместо указанного указателя.