Выравнивание по 4-байтным границам

Недавно я подумал о выравнивании... Это то, что мы обычно не рассматриваем, но я понял, что некоторые процессоры требуют выравнивания объектов по 4-байтным границам. Что именно это означает, и какие конкретные системы имеют требования к выравниванию?

Предположим, что у меня есть произвольный указатель:

unsigned char* ptr

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

double d = **((double*)ptr);

Это вызовет проблемы?

Ответ 1

Это может определенно вызвать проблемы в некоторых системах.

Например, в системах на основе ARM вы не можете адресовать 32-битное слово, которое не выровнено по 4-байтовой границе. Это приведет к исключению нарушения доступа. На x86 вы можете получить доступ к таким не выровненным данным, хотя производительность немного страдает, поскольку два слова должны извлекаться из памяти вместо одного.

Ответ 2

В этом документе Справочное руководство Intel x86/x64 говорится о выравниваниях:

4.1.1 Выравнивание слов, двойных слов, четырехугольников и двойных четырехъядерных букв

Слова, двойные слова и квадранты не нужно выравнивать в памяти на естественные границы. Естественный границы слов, двойные слова, и четырехъядерные слова имеют четное число адреса, адреса равномерно делимые на четыре, и адреса равномерно делится на восемь соответственно. Однако для повышения эффективности программ, структур данных (особенно стеки) должны быть выровнены по естественным когда это возможно. причина в том, что процессор требуется два обращения к памяти, чтобы сделать неприглаженный доступ к памяти; выровненный для доступа требуется только одна память доступ. Слово или операнд двойного слова который пересекает 4-байтовую границу или quadword операнд, который пересекает Рассматривается 8-байтовая граница неприсоединен и требует двух отдельных циклов памяти для доступа.

Некоторые инструкции, которые работают двойные квадранты требуют памяти операнды, которые будут выровнены по естественному граница. Эти инструкции генерируют исключение общей защиты (#GP) если указан неглавный операнд. Естественная граница для двойной quadword - любой адрес равномерно делится на 16. Другие инструкции которые работают на двойных четырехъядерных языках разрешить неравномерный доступ (без генерирование общей защиты исключение). Однако дополнительная память для доступа к неизмененные данные из памяти.

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

Ответ 3

Да, это может вызвать ряд проблем. Стандарт С++ на самом деле не гарантирует, что он будет работать. Вы не можете просто произвольно использовать типы указателей.

Когда вы накладываете указатель char на двойной указатель, он использует reinterpret_cast, который применяет отображение, определенное реализацией. Вам не гарантируется, что результирующий указатель будет содержать один и тот же шаблон бита или что он укажет на тот же адрес или, ну, что-нибудь еще. Более практично, вы также не гарантируете, что значение, которое вы читаете, правильно выровнено. Если данные были записаны в виде серии символов, то они будут использовать требования к выравниванию char.

Что касается того, что означает выравнивание, по сути, только начальный адрес значения должен быть делит на размер выравнивания. Адрес 16 выровнен по границам 1, 2, 4, 8 и 16 байтов, например, так что на типичных ЦП значения этих размеров могут быть сохранены там.

Адрес 6 не выровнен по 4-байтовой границе, поэтому мы не должны хранить 4-байтовые значения там.

Стоит отметить, что даже на процессоре, который не обеспечивает или не требует выравнивания, вы, как правило, все еще получаете значительное замедление от доступа к значениям без знака.

Ответ 4

Выравнивание влияет на компоновку структур. Рассмотрим эту структуру:

struct S {
  char a;
  long b;
};

В 32-битном ЦП макет этой структуры будет часто:

a _ _ _ b b b b

Требование состоит в том, что 32-битное значение должно быть выровнено на 32-битной границе. Если структура изменяется следующим образом:

struct S {
  char a;
  short b;
  long c;
};

макет будет таким:

a _ b b c c c c

16-разрядное значение выравнивается по 16-разрядной границе.

Иногда вы хотите упаковать структуры, возможно, если вы хотите сопоставить структуру с форматом данных. Используя параметр компилятора или, возможно, #pragma, вы можете удалить лишнее пространство:

a b b b b
a b b c c c c

Однако доступ к неуравновешенному члену упакованной структуры часто будет намного медленнее на современных процессорах или может даже привести к исключению.

Ответ 5

Да, это может вызвать проблемы.

4-выравнивание просто означает, что указатель, если он рассматривается как числовой адрес, кратен 4. Если указатель не кратен требуемому выравниванию, то он не выравнивается. Существует две причины, по которым компиляторы устанавливают ограничения выравнивания для определенных типов:

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

Если вы в этом случае (1), а double - в 4-выровненных, и вы пытаетесь использовать код с указателем char *, который не имеет 4-выровненности, то скорее всего вы получите аппаратную ловушку. Некоторые аппаратные средства не ловушки. Он просто загружает бессмысленное значение и продолжается. Однако стандарт С++ не определяет, что может произойти (поведение undefined), поэтому этот код может привести к выходу компьютера из строя.

На x86 вы никогда не будете в случае (1), потому что стандартные инструкции загрузки могут обрабатывать невыровненные указатели. В ARM нет неуравновешенных нагрузок, и если вы попытаетесь выполнить одно из них, то ваша программа выйдет из строя (если вам повезет. Некоторые ARM молча проваливаются).

Возвращаясь к вашему примеру, возникает вопрос, почему вы пытаетесь сделать это с char *, который не выровнен по четырем. Если вы успешно написали двойной номер через double *, вы сможете его прочитать. Поэтому, если у вас изначально был "правильный" указатель на double, который вы добавили в char *, и теперь вы отбрасываете назад, вам не нужно беспокоиться о выравнивании.

Но вы сказали произвольный char *, поэтому я думаю, что не то, что у вас есть. Если вы прочитали фрагмент данных из файла, который содержит сериализованный двойной, то вы должны убедиться, что требования к выравниванию для вашей платформы выполнены, чтобы сделать это. Если у вас есть 8 байтов, представляющих двойной в некотором формате файла, то вы не можете просто прочитать его волей-неволей в буфере char * при любом смещении, а затем применить к double *.

Самый простой способ сделать это - убедиться, что вы прочитали данные файла в подходящей структуре. Вам также помогает тот факт, что распределения памяти всегда выровнены с максимальным требованием к выравниванию любого типа, который достаточно велик для их хранения. Поэтому, если вы выделяете буфер, достаточно большой для того, чтобы содержать double, тогда начало этого буфера имеет любое выравнивание, требуемое двойным. Итак, вы можете прочитать 8 байт, представляющих двойной в начале буфера, бросить (или использовать объединение) и прочитать двойной выход.

В качестве альтернативы вы можете сделать что-то вроде этого:

double readUnalignedDouble(char *un_ptr) {
    double d;
    // either of these
    std::memcpy(&d, un_ptr, sizeof(d));
    std::copy(un_ptr, un_ptr + sizeof(d), reinterpret_cast<char *>(&d));
    return d;
}

Это гарантированно действует (если un_ptr действительно указывает на байты действительного двойного представления для вашей платформы), поскольку double является POD и, следовательно, может быть скопирован побайтно. Это может быть не самое быстрое решение, если у вас много парных нагрузок.

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

Наконец, ничего не нужно делать с выравниванием, у вас также есть строгий псевдоним, о котором можно беспокоиться, если вы получили этот char * через приведение из указателя, который не совместим с алиасом с double *. Алиасирование действует только между char * и всем остальным.

Ответ 6

На x86 он всегда будет работать, конечно, более эффективно при выравнивании.

Но если вы многозначительно следите за чтением и разрывом. С 64-битным значением вам понадобится машина x64, которая даст вам атомарное чтение и запись между потоками.
Если вы говорите, что вы читаете значение из другого потока, когда говорят, что оно увеличивается с 0x00000000.FFFFFFFF и 0x00000001.00000000, то другой поток может теоретически читать либо 0, либо 1FFFFFFFF, особенно если ЕСЛИ СКАЗАТЬ значение STRADDLED A CACHE-LINE. Я рекомендую Duffy "Параллельное программирование в Windows" для приятного обсуждения моделей памяти, даже упоминая о том, как выстроить выходы на многопроцессорных системах, когда dot-net делает GC. Вы хотите держаться подальше от Itanium!

Ответ 7

SPARC (компьютеры Solaris) - это еще одна архитектура (по крайней мере, некоторые из прошлых времен), которая захлестнет (выдаст ошибку SIGBUS), если вы попытаетесь использовать неизмененное значение.

Добавление к Martin York, malloc также выровнено по максимально возможному типу, то есть оно безопасно для всего, как "новое". На самом деле часто "новый" просто использует malloc.

Ответ 8

Примером требования к элегированию является использование инструкций по векторизации (SIMD). (Его можно использовать без арифметики, но намного быстрее, если вы используете какую-то инструкцию, требующую выравнивания).

Ответ 9

Принудительное выравнивание памяти гораздо чаще встречается в RISC, основанных на архитектуре, таких как MIPS.
Главное мышление для этих типов процессоров, AFAIK, действительно является проблемой скорости.
Методика RISC заключалась в том, чтобы иметь набор простых и быстрых инструкций (обычно один цикл памяти для каждой инструкции). Это не означает, что он имеет меньше инструкций, чем процессор CISC, тем более, что он имеет более простые и быстрые инструкции.
Многие процессоры MIPS, хотя 8-байтовый адресный, были бы выровнены по слову (обычно 32 бита, но не всегда), затем маскируют соответствующие биты. Идея состоит в том, что быстрее выполнять выравниваемую маску + бит, чем пытаться выполнить невысокую нагрузку. Обычно (и, конечно же, это действительно зависит от набора микросхем), при невыложенной нагрузке генерируется ошибка шины, поэтому процессоры RISC будут предлагать команду "неравномерная загрузка/сохранение", но это часто будет намного медленнее, чем соответствующая выровненная загрузка/хранилище,

Конечно, это все еще не отвечает на вопрос о том, почему они делают это. Какое преимущество дает вам выравнивание слов памяти? Я не эксперт по аппаратным средствам, и я уверен, что кто-то здесь может дать лучший ответ, но мои две лучшие догадки:
1. Быстрее извлечь из кеша при выравнивании слова, поскольку многие кеши организованы в кеш-строки (что-то от 8 до 512 байт), а поскольку кэш-память обычно намного дороже, чем оперативная память, вы хотите максимально использовать этого. 2. Возможно, гораздо быстрее получить доступ к каждому адресу памяти, так как он позволяет вам читать "Режим серийной съемки" (например, после получения следующего последовательного адреса)

Обратите внимание, что ни одно из вышеперечисленных случаев строго невозможно в несогласованных магазинах, я предполагаю (хотя я не знаю), что многие из них сводятся к выбору дизайна оборудования и стоимости