Выравнивание памяти по конкретным адресным границам в C / C ++ все еще улучшает производительность x86? - PullRequest
0 голосов
/ 05 января 2019

Во многих руководствах по разработке с малой задержкой обсуждается выравнивание распределения памяти по конкретным границам адресов:

https://github.com/real-logic/simple-binary-encoding/wiki/Design-Principles#word-aligned-access

http://www.alexonlinux.com/aligned-vs-unaligned-memory-access

Однако вторая ссылка относится к 2008 году. Обеспечивает ли выравнивание памяти по границам адресов повышение производительности процессоров Intel в 2019 году? Я думал, что процессоры Intel больше не несут штраф за задержку доступа к невыровненным адресам? Если нет, то при каких обстоятельствах это должно быть сделано? Должен ли я выровнять каждую переменную стека? Переменная члена класса?

У кого-нибудь есть примеры, когда они обнаружили значительное улучшение производительности при выравнивании памяти?

Ответы [ 2 ]

0 голосов
/ 05 января 2019

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

0 голосов
/ 05 января 2019

Штрафы, как правило, небольшие, но они пересекают границу страницы 4 КБ на процессорах Intel, прежде чем Skylake получит большой штраф (~ 150 циклов). Как я могу точно оценить скорость невыровненного доступа на x86_64 содержит некоторые подробности о фактических эффектах пересечения границы строки кэша или границы 4 КБ. (Это применимо, даже если загрузка / хранение находится внутри одной огромной страницы 2M или 1G, потому что оборудование не может знать об этом, пока после того, как он не запустит процесс проверки TLB дважды.) Например, в массиве double, который был только 4 -байт выровнен, на границе страницы будет один дубль, который будет равномерно распределен между двумя страницами 4k. То же самое для каждой границы строки кэша.

Обычные разбиения строки кэша, которые не пересекают страницу 4 КБ, стоят ~ 6 дополнительных циклов задержки для Intel (всего 11c на Skylake, против 4 или 5c для обычного попадания L1d) и требуют дополнительной пропускной способности (которая может иметь значение в коде, который обычно выдерживает около 2 загрузок за такт.)

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

Должен ли я выравнивать каждую переменную стека?

Нет, компилятор уже сделает это за вас . Соглашения о вызовах x86-64 поддерживают 16-байтовое выравнивание стека, поэтому они могут получить любое выравнивание до него бесплатно, включая 8-байтовые массивы int64_t и double.

Также помните, что большинство локальных переменных хранятся в регистрах большую часть времени, когда они интенсивно используются. Если переменная не равна volatile или вы компилируете без оптимизации, значение не нужно сохранять / перезагружать между доступами.

Обычные ABI также требуют естественного выравнивания (выровненного по его размеру) для всех типов примитивов, поэтому даже внутри структур и т. Д. Вы получите выравнивание, и один тип примитивов никогда не будет охватывать кеш граница (исключение: для i386 System V требуется только 4-байтовое выравнивание для int64_t и double. За пределами структур компилятор решит дать им больше выравнивания, но внутри структур он не может изменить правила компоновки. Поэтому объявите свои структуры в порядке, в котором сначала размещаются 8-байтовые элементы или, по крайней мере, они располагаются таким образом, чтобы получить выравнивание по 8 байт. Возможно, используйте alignas(8) на таких элементах структуры, если вы заботитесь о 32-битном коде, если его еще нет члены, которые требуют такого большого выравнивания.)

x86-64 System V ABI (все платформы, отличные от Windows) требует выравнивания массивов по 16, если они имеют автоматическое или статическое хранилище вне структуры. maxalign_t равно 16 для x86-64 SysV, поэтому malloc / new возвращает 16-байтовую выровненную память для динамического выделения. Ориентация на gcc Windows также выравнивает массивы стека, если в этой функции она автоматически векторизируется.


(Если вы вызываете неопределенное поведение, нарушая требования выравнивания ABI, это часто не приводит к изменению производительности. Обычно это не вызывает проблем с корректностью x86, но это может привести к сбоям для типа SIMD, и с автоматической векторизацией скалярных типов . Например, Почему при выравнивании доступа к памяти mmap иногда происходит ошибка на AMD64? . Так что, если вы намеренно смещаете данные, убедитесь, что вы не обращаетесь к ним с какими-либо указатель шире char*. например используйте memcpy(&tmp, buf, 8) с uint64_t tmp для выравнивания нагрузки. GCC может через это автоматически делать автоколонку, IIRC.)


Иногда вам может потребоваться alignas(32) или 64 для больших массивов, если вы компилируете с включенным AVX или AVX512 . Для цикла SIMD над большим массивом (который не помещается в кэш L2 или L1d), с AVX / AVX2 (32-байтовыми векторами) обычно достигается почти нулевой эффект от проверки его выравнивания на 32 в Intel Haswell / Skylake. Узкие места в памяти в данных, поступающих с L3 или DRAM, дадут единицам загрузки / хранения ядра и времени кэш-памяти L1d для выполнения множественного доступа изнутри, даже если каждая другая загрузка / хранение пересекает границу строки кэша.

Но с AVX512 на Skylake-сервере на практике наблюдается значительный эффект для 64-байтового выравнивания массивов, даже с массивами, которые поступают из кэша L3 или, возможно, DRAM . Я забыл подробности, я уже давно не видел пример, но, может быть, от 10 до 15% даже для цикла с привязкой к памяти? Каждая 64-байтовая векторная загрузка и хранение пересекает 64-байтовую границу строки кэша, если они не выровнены.

В зависимости от цикла вы можете обрабатывать выровненные входы, выполняя первый вектор, возможно, не выровненный, затем зацикливаясь на выровненных векторах до последнего выровненного вектора. Другой возможно перекрывающийся вектор, который идет в конец массива, может обрабатывать последние несколько байтов. Это прекрасно работает для цикла копирования и обработки, где можно повторно копировать и повторно обрабатывать одни и те же элементы в перекрытии, но есть и другие методы, которые можно использовать для других случаев, например, скалярная петля до границы выравнивания, более узких векторов или маскирования. Если ваш компилятор выполняет авто-векторизацию, выбор зависит от компилятора. Если вы вручную векторизуете с помощью встроенных функций, вы можете выбирать. Если массивы обычно выровнены, хорошей идеей будет просто использовать невыровненные нагрузки (которые не штрафуют, если указатели выровнены во время выполнения), и позволить аппаратному обеспечению обрабатывать редкие случаи невыровненных входов, чтобы не было никаких накладных расходов на программное обеспечение. выровненные входы.

...