Чтение кешлайна против его частей в Intel x86 - PullRequest
0 голосов
/ 21 февраля 2019

На этот вопрос может не быть окончательного ответа, но требуется общий совет в этой области.Дайте мне знать, если это не по теме.Если у меня есть код, который читает из строки кэша, которой нет в кеше L1 текущего ЦП, и читает, отправляются в удаленный кеш, скажем, объект поступает из удаленного потока, который только что записал в него, и, следовательно, имеет кешлайн в измененном режиме.Есть ли какие-то дополнительные затраты на чтение всей кеш-линии, а не только ее части?Или что-то подобное может быть полностью распараллелено?

Например, учитывая следующий код (предположим, что foo() ложь - это какая-то другая единица перевода и непрозрачна для оптимизатора, LTO не задействован)

struct alignas(std::hardware_destructive_interference_size) Cacheline {
    std::array<std::uint8_t, std::hardware_constructive_interference_size> bytes;
};

void foo(std::uint8_t byte);

Есть ли ожидаемая разница в производительности между этим

void bar(Cacheline& remote) {
  foo(remote.bytes[0]);
}

и

void bar(Cacheline& remote) {
    for (auto& byte : remote.bytes) {
        foo(byte);
    }
}

Или это что-то, что, скорее всего, окажет небольшое влияние?Передается ли полная кэшированная строка текущему процессору до завершения чтения?Или же процессор может распараллелить чтение и удаленную выборку из кэша (в этом случае может оказать влияние ожидание передачи всей строки кэша)?


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

1 Ответ

0 голосов
/ 21 февраля 2019

Если вам нужно прочитать какие-либо байты из строки кэша, ядро ​​должно захватить всю строку кэша в общем состоянии MESI.В Haswell и более поздних версиях путь данных между кэшем L2 и L1d имеет ширину 64 байта (* https://www.realworldtech.com/haswell-cpu/5/),, поэтому буквально вся строка поступает в одно и то же время, в одном и том же тактовом цикле .выгодно только читать младшие 2 байта против старшего и младшего байта или байта 0 и байта 32.

То же самое по существу верно для более ранних процессоров: строки по-прежнему отправляются целиком и поступают вимпульс может составлять от 2 до 8 тактов. (AMD Multi-Socket K10 может даже создать разрыв между 8-байтовыми границами при отправке строки между ядрами на разных сокетах через HyperTransport, так что это разрешило чтение из кеша и /или запись происходит между циклами отправки или получения строки.)

(Разрешение начала загрузки при получении нужного ей байта называется "ранним перезапуском" в терминологии архитектуры ЦП .соответствующая уловка - «сначала критическое слово», когда чтение из DRAM запускает пакет со словом, необходимым для нагрузки по требованию, которая его сработала.Актер в современных процессорах x86 с путями данных шириной, равной строке кэша или близкой к ней, где строка может появиться в 2 цикла.Вероятно, не стоит посылать слово внутри строки как часть запросов на строки кэша, даже в тех случаях, когда запрос пришел не только из предустановки HW.)

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


И любая отдельная нагрузка, которая не пересекает границу строки кэша, имеет такую ​​же стоимость, как и любая другая, будь то 1 байт или 32 байта .Или 64 байта с AVX512.Вот некоторые исключения из этого:

  • не выровненные 16-байтовые загрузки перед Nehalem: movdqu декодирует до дополнительных операций, даже если адрес выровнен .
  • 32-байтовые загрузки AVB SnB / IvB занимают 2 цикла в одном и том же порту загрузки для 16-байтовых половин.
  • AMD может иметь некоторые штрафы за пересечение 16-байтовых или 32-байтовых границ сзагрузка без выравнивания.
  • Процессоры AMD до того, как Zen2 разделит 256-битные (32-байтовые) операции AVX / AVX2 на две 128-битные половины, так что это правило одинаковой стоимости для любого размера применяется только до 16 байтов на AMD,Или до 8 байтов на некоторых очень старых процессорах, которые разбивают 128-битные векторы на две половины, такие как Pentium-M или Bobcat.
  • целочисленные нагрузки могут иметь задержку использования нагрузки на 1 или 2 цикла меньше, чем векторные нагрузки SIMD,Но вы говорите о дополнительных затратах на выполнение большего количества загрузок, поэтому нового адреса ждать не приходится.(По-видимому, просто другое немедленное смещение из того же базового регистра. Или что-то дешевое для расчета.)

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


Как только вы заплатите за потерю кеша, остальная часть строки будет перегрета в кеше L1d, пока какой-то другой поток не захочет записать его и свой RFO (чтение для владения) делает вашу строку недействительной.

Вызывать не встроенную функцию 64 раза вместо одного, очевидно, дороже, но я думаю, что это просто плохой пример того, что вы пытаетесьпросить.Может быть, лучшим примером были бы две int нагрузки против двух __m128i нагрузок?

Промахи в кеше - не единственное, что стоит времени, хотя они могут легко доминировать.Но, тем не менее, вызов + ret занимает по крайней мере 4 такта (https://agner.org/optimize/ таблицы команд для Haswell показывают, что каждая из вызовов / ретрансляторов имеет пропускную способность по одному на 2 такта, и я думаю, что это правильно), поэтому зацикливание и вызов функции 64 раза на 64 байтах строки кэша занимает не менее 256 тактов .Это, вероятно, больше, чем задержка между ядрами на некоторых процессорах.Если бы он мог встроиться и автоматически векторизоваться с помощью SIMD, дополнительные издержки за пределами промаха кэша были бы намного меньше, в зависимости от того, что он делает., как 2 за тактовую пропускную способность.Загрузка в качестве операнда памяти для инструкции ALU (вместо необходимости отдельного mov) может декодироваться как часть того же самого UOP, что и инструкция ALU, поэтому даже не стоит дополнительной входной полосы пропускания.


Использование более простого в декодировании формата, который всегда заполняет строку кэша, вероятно, является выигрышным для вашего варианта использования. Если это не означает повторение циклов больше раз.Когда я говорю «проще декодировать», я имею в виду меньшее количество шагов в вычислениях, а не простой исходный код (например, простой цикл, который выполняет 64 итерации).

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