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

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

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

unsigned char* ptr

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

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

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

9 ответов

Решение

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

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

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

4.1.1 Выравнивание слов, двойных слов, четырех слов и двойных слов

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Самый простой способ сделать это - убедиться, что вы прочитали данные файла в подходящую структуру. Вам также помогает тот факт, что выделения памяти всегда выровнены по требованию максимального выравнивания любого типа, который они достаточно велики, чтобы вместить. Таким образом, если вы выделяете буфер, достаточно большой, чтобы содержать 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-битными байтами, или о некоторых других необычных свойствах, где могут быть не значения биты в хранимом представлении double. Но вы на самом деле не спрашивали о файлах, я просто придумал это в качестве примера, и в любом случае эти платформы гораздо реже, чем вопрос, о котором вы спрашиваете, что для double требуется требование выравнивания.

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

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

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

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

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

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

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

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

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

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

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

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

Другие вопросы по тегам