Если вам нужно прочитать какие-либо байты из строки кэша, ядро должно захватить всю строку кэша в общем состоянии 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 итерации).