Пользовательский распределитель и выравнивание памяти - PullRequest
0 голосов
/ 07 мая 2020

Я пытаюсь реализовать собственный распределитель для работы с контейнерами std на основе требований здесь: https://en.cppreference.com/w/cpp/named_req/Allocator

Сейчас я пытаюсь реализовать линейный распределитель, и я ' m с трудом справляется с выравниванием памяти. После выделения блока памяти мне интересно, сколько заполнения мне нужно между каждым объектом в блоке, чтобы оптимизировать чтение / запись процессора. Я не уверен, должно ли выравнивание адресов делиться

  • на размер слова процессора (4 байта на 32-битной машине и 8 байтов на 64-битной машине)
  • sizeof(T)
  • alignof(T)

Я читал разные ответы в разных местах. Например, в этом вопросе принятые ответы говорят:

Обычное практическое правило (прямо из руководств по оптимизации Intel и AMD) состоит в том, что каждый тип данных должен быть выровнен самостоятельно. размер. Int32 должен быть выровнен по 32-битной границе, int64 по 64-битной границе и так далее. Символ подойдет где угодно.

Таким образом, из этого ответа похоже, что выравнивание адреса должно делиться на sizeof(T).

На этот вопрос второй ответ гласит, что:

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

Итак, из этого ответа похоже, что выравнивание адреса должно делиться на размер слова процессора.

Итак, я вижу некоторые противоречивые заявления о том, как оптимизировать выравнивание данных для чтения / записи процессора, и я не уверен, что я что-то неправильно понимаю или есть какие-то неправильные ответы? Может быть, кто-нибудь сможет разъяснить мне, на что должно делиться выравнивание адреса.

Ответы [ 2 ]

2 голосов
/ 07 мая 2020

В качестве общего практического правила (то есть делайте это, если у вас нет веской причины поступить иначе), вы хотите выровнять элементы данного типа C ++ по их выравниванию, т. Е. alignof(T). Если тип хочет быть выровнен по 32-битной границе (как реализовано int в наиболее распространенной реализации С ++), он будет демонстрировать подходящее (4-байтовое) выравнивание.

Конечно, между базовых адресов двух разных объектов типа T должно быть не менее sizeof(T) байт пространства, что обычно будет целым числом, кратным его выравниванию (на самом деле довольно сложно передать перевыровненный тип объекту шаблонную функцию, так как она удалит любой внешний атрибут alignas).

В большинстве случаев использования вы будете в порядке, выполнив следующие действия: Найдите первый базовый адрес в вашем базовом хранилище, который выровнен на alignof(T), а затем на go вперед с шагом sizeof(T).

Таким образом, вы будете полагаться на то, что пользователи вашего распределителя сообщат вам, чего они хотят. Это именно то, что вам нужно, поскольку оптимизатор может полагаться на знания о выравнивании и, например, выдавать выровненные по SSE нагрузки для массивов с плавающей запятой двойной точности, что приведет к ошибке sh в вашей программе, если они выровнены неправильно.

Спуск в кроличью нору

Это приводит к следующим возможным ситуациям:

  1. Простой шрифт, имеет длину слова и выравнивание слов (например, int с sizeof(int) = 4 и alignof(int) = 4):
sizeof(T) = 4 and alignof(T) = 4
 0  1  2  3  4  5  6  7  8  9  A  B  C  D  E  F 
[aaaaaaaaaa][bbbbbbbbbb][cccccccccc][dddddddddd]
Типы, размер которых кратен его выравниванию (например, using T = int[2])
sizeof(T) = 8 and alignof(T) = 4
 0  1  2  3  4  5  6  7  8  9  A  B  C  D  E  F 
[aaaaaaaaaaaaaaaaaaaaaa][bbbbbbbbbbbbbbbbbbbbbb]
Типы с чрезмерным выравниванием, у которых выравнивание больше, чем у размера (например, using T = alignas(8) char[3]). Вот драконы!
sizeof(T) = 3 and alignof(T) = 8
 0  1  2  3  4  5  6  7  8  9  A  B  C  D  E  F 
[aaaaaaa]               [bbbbbbb]

Обратите внимание, что в приведенном выше примере есть неиспользуемое пространство . Это необходимо, поскольку объекты, выровненные по 8-байтовой границе, нельзя размещать в другом месте, что может привести к потерям. Чаще всего для таких типов используются опции c, специфичные для ЦП, например, для предотвращения ложного совместного использования .

Наконец, есть немного странный случай, когда размер объектов больше, чем целое число, кратное их выравниванию (например, using T = alignas(4) char[5];). По сути, это всего лишь небольшое расширение предыдущего примера с избыточным числом типов:
sizeof(T) = 5 and alignof(T) = 4
 0  1  2  3  4  5  6  7  8  9  A  B  C  D  E  F 
[aaaaaaaaaaaaa]         [bbbbbbbbbbbbb]

Хотя выравнивание позволило бы разместить второй объект по базовому адресу 4, уже существует

Собирая все эти примеры вместе, количество байтов, которое должно быть между базовыми адресами двух объектов типа T, равно:

inline auto object_distance = sizeof(T) % alignof(T) == 0 ? sizeof(T) : sizeof(T) + (alignof(T) - sizeof(T) % alignof(T));
1 голос
/ 07 мая 2020

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

Точно нулевое заполнение между объектами ; вам не разрешено добавлять отступы. В модели распределителя стандартной библиотеки C ++ ваш метод allocator<T>::allocate(count) необходим для выделения достаточного пространства для хранения массива count объектов типа T. Массивы в C ++ плотно упакованы; смещение от одного T в массиве к другому T должно быть sizeof(T).

Таким образом, вы не можете вставлять отступы между объектами в выделенном хранилище. Вы можете вставить отступ в начале блока памяти, который вы выделяете, чтобы вы могли быть точными с alignof(T) (что ваш allocator<T>::allocate также должен учитывать). Но возвращаемый указатель должен быть указателем на выровненное хранилище для T s. Поэтому, если у вас есть заполнение перед выделением, вам понадобится способ отменить заполнение при вызове deallocate, поскольку он получает только выровненный адрес хранилища.

Когда дело доходит до выравнивания структур, которые содержат фундаментальные типы, вы полагаетесь на компилятор, чтобы наложить свои требования на выравнивание этих структур. Итак, для этого определения:

struct U
{
  std::int32_t i;
  std::int64_t j;
};

Если компилятор считает, что было бы более оптимальным для int64_t быть с 8-байтовым выравниванием, то компилятор вставит соответствующий отступ между i и j дюйм U. sizeof(U) будет 16, а alignof(U) будет 8.

Создание такого выравнивания - не ваша работа, и вам не разрешено делать это для компилятора. Вы должны просто уважать выравнивание любого типа, который вы указываете в своих allocator<T>::allocate звонках.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...