TL; DR:
Случай 1: буфер, который умещается в L1D. Конструктор вектора или вызов std::fill
полностью поместит буфер в L1D. В этом случае узким местом является пропускная способность конвейера в 1 хранилище и кэш L1D.
Случай 2: буфер, который вписывается в L2. Конструктор вектора или вызов std::fill
полностью поместит буфер в L2. Однако L1 должен записывать грязные строки обратно в L2, и между L1D и L2 есть только один порт. Кроме того, линии должны быть извлечены из L2 в L1D. Пропускная способность 64B / цикл между L1D и L2 должна быть в состоянии легко справиться с этим, возможно, с периодическим конфликтом (подробнее см. Ниже). Таким образом, в целом узкое место такое же, как и в случае 1. Конкретный размер используемого вами буфера, около 40 КБ, не подходит для L1D Intel и последних процессоров AMD, но подходит для L2. Несмотря на то, что в случае одновременной многопоточности (SMT), может существовать дополнительная конкуренция со стороны другого логического ядра.
Случай 3: буфер, который не помещается в L2. Строки должны быть извлечены из L3 или памяти. Устройство предварительной выборки L2 DPL может отслеживать хранилища и предварительную выборку буфера в L2, тем самым уменьшая длительную задержку. Единственный порт L2 является узким местом вместе с буферами обратной записи и заполнения L1. Это серьезно, особенно когда буфер не помещается в L3, где межсоединение также может быть на критическом пути. Пропускная способность 1 хранилища слишком велика для подсистемы кеша. Два наиболее важных счетчика производительности: L1D_PEND_MISS.REQUEST_FB_FULL
и RESOURCE_STALLS.SB
.
Во-первых, обратите внимание, что конструктор (который, вероятно, будет встроен) vector
сам инициализирует элементы в ноль, вызывая memset
внутри. memset
в основном делает то же самое, что и ваш цикл, но он очень оптимизирован. Другими словами, с точки зрения обозначения big-O, оба линейны по количеству элементов, но memset
имеет меньший постоянный коэффициент. Кроме того, std::fill
также внутренне вызывает memset
, чтобы установить все элементы в ноль (снова). std::fill
также, вероятно, будет встроен (при включенной правильной оптимизации). Следовательно, у вас действительно есть три цикла в этом куске кода. Было бы более эффективно инициализировать ваш вектор, используя std::vector<int> vec(10000u, 5)
. Теперь давайте перейдем к микроархитектурному анализу цикла. Я буду обсуждать только то, что ожидается от современных процессоров Intel, в частности от Haswell и Skylake 1 .
Давайте внимательно рассмотрим код:
00007FF6A4521080 inc edx
00007FF6A4521082 mov dword ptr [rcx+rax*4],5
00007FF6A4521089 mov eax,edx
00007FF6A452108B cmp rax,r9
00007FF6A452108E jb main+80h (07FF6A4521080h)
Первая инструкция будет декодирована в один моп. Вторая инструкция будет декодирована в два мопа, которые слиты во внешнем интерфейсе. Третья инструкция представляет собой перемещение из регистра в регистр и является кандидатом на исключение из перемещения на этапе переименования регистра. Трудно сказать наверняка, будет ли этот шаг устранен без выполнения кода 3 . Но даже если оно не будет устранено, инструкции будут отправлены следующим образом 2 :
dispatch cycle | allocate cycle
cmp rax,r9 macro-fused | inc edx (iteration J+3)
jb main+80h (07FF6A4521080h) (iteration J) | mov dword ptr [rcx+rax*4],5 (iteration J+3)
mov dword ptr [rcx+rax*4],5 (iteration J+1)| mov eax,edx (iteration J+3)
mov eax,edx (iteration J+1)| cmp rax,r9 macro-fused
inc edx (iteration J+2)| jb main+80h (07FF6A4521080h) (iteration J+3)
---------------------------------------------------------|---------------------------------------------------------
cmp rax,r9 macro-fused | inc edx (iteration J+4)
jb main+80h (07FF6A4521080h) (iteration J+1)| mov dword ptr [rcx+rax*4],5 (iteration J+4)
mov dword ptr [rcx+rax*4],5 (iteration J+2)| mov eax,edx (iteration J+4)
mov eax,edx (iteration J+2)| cmp rax,r9 macro-fused
inc edx (iteration J+3)| jb main+80h (07FF6A4521080h) (iteration J+4)
Инструкции cmp
и jb
будут макрофузированы в один моп. Таким образом, общее количество мопов равно 4 в слитом домене и 5 в неиспользованном домене. Среди них ровно один прыжок. Следовательно, итерация одного цикла может быть выдана за цикл.
Из-за зависимости между inc
и mov
хранилищем эти две инструкции не могут быть отправлены в одном и том же цикле. Тем не менее, inc
из предыдущей итерации можно отправить с помощью мопов из предыдущей итерации.
Существует четыре порта (p0, p1, p5, p6), к которым можно подключить inc
и mov
. Существует ровно один порт, p6, для прогнозируемого принятого cmp/jb
. Есть три порта (p2, p3, p7) для STA uop mov dword ptr [rcx+rax*4],5
и один порт, p4, для STD uop. (Хотя p7 не может обработать указанный режим адресации.) Поскольку существует только один порт для каждого, максимальная пропускная способность, которая может быть достигнута, составляет 1 итерацию за цикл.
К сожалению, пропускная способность будет хуже; многие магазины пропустят в L1D. Устройства предварительной выборки L1D не способны выполнять предварительную выборку строк в состоянии исключительной когерентности и не отслеживают запросы магазина. Но, к счастью, многие магазины будут объединены. Последовательные хранилища в цикле нацелены на последовательные расположения в виртуальном адресном пространстве. Поскольку строка имеет размер 64 байта, а каждое хранилище имеет размер 4 байта, каждые 16 последовательных хранилищ располагаются в одной и той же строке кэша. Эти магазины могут быть объединены в буфере магазина, но они не будут, потому что магазины уйдут в отставку как можно раньше, как только они окажутся на вершине ROB. Тело цикла довольно маленькое, поэтому маловероятно, что более 16 из 16 хранилищ будут объединены в буфере хранилища. Однако, когда комбинированный запрос хранилища будет выдан L1D, он пропустит и будет выделен LFB, который также поддерживает объединение хранилищ. Устройство предварительной выборки DPL-кэша L2 способно отслеживать запросы RFO, так что, надеюсь, мы почти всегда попадем в L2. Но это займет не менее 10-15 циклов, чтобы получить линию от L2 до L1 в. Тем не менее, RFO может быть отправлено досрочно, до того, как магазин действительно будет зафиксирован. В то же время, скорее всего, необходимо удалить грязную строку из L1, чтобы освободить место для входящей строки для записи. Выселенная строка будет записана в буфер обратной записи.
Трудно предсказать, каким будет общий эффект без запуска кода. Два наиболее значимых счетчика производительности: L1D_PEND_MISS.REQUEST_FB_FULL
и RESOURCE_STALLS.SB
.
.
L1D имеет только один порт хранилища шириной 16, 32 и 64 байта на Ivy Bridge, Haswell и Skylake соответственно. Таким образом, магазины будут совершены в этих гранулярности. Но один LFB всегда может содержать полную 64-байтовую строку кэша.
Общее количество слитков в магазине равно количеству элементов (в данном случае 1 миллион). Чтобы получить необходимое количество LFB, разделите на 16, чтобы получить 62500 LFB, что соответствует количеству RFO для L2. Потребуется 16 циклов, прежде чем потребуется еще один LFB, потому что за цикл может быть отправлено только одно хранилище. Пока L2 может доставить целевую линию в течение 16 циклов, мы никогда не будем блокировать LFB, и достигнутая пропускная способность будет близка к 1 итерации за цикл, или, с точки зрения IPC, 5 инструкций за цикл. Это возможно только в том случае, если мы почти всегда попадаем в L2 своевременно. Любая постоянная задержка в кеше или памяти значительно снизит пропускную способность ниже этой. Это может выглядеть примерно так: пакет из 16 итераций будет выполняться быстро, затем конвейер останавливается на LFB на некоторое количество циклов. Если это число равно задержке L3 (около 48 циклов), то пропускная способность будет составлять около 1 итерации за 3 цикла (= 16/48).
L1D имеет ограниченное количество (6?) Буферов обратной записи для хранения выселенных линий. Кроме того, L2 имеет только один 64-байтовый порт, который используется для всей связи между L1D и L2, включая обратные записи и RFO. Доступность буферов обратной записи также может быть на критическом пути. В этом случае количество LFB также является узким местом, поскольку LFB не будет записан в кэш, пока не будет доступен буфер обратной записи. Если нет, LFB заполнятся быстро, особенно если средство предварительной выборки L2 DPL смогло доставить линии своевременно. Очевидно, что потоковая кэш-память WB в L1D очень неэффективна.
Если вы запускаете код, вам также необходимо учесть два вызова на memset
.
(1) На Sandy Bridge и Ivy Bridge, , инструкция mov dword ptr [rcx+rax*4],5
будет не ламинирована , что приведет к 5 мопам за итерацию в слитой области. Таким образом, интерфейс может быть на критическом пути.
(2) Или что-то в этом роде, в зависимости от того, получает ли первая инструкция первой итерации цикла первый слот распределителя. Если нет, показанные числа итераций должны быть соответственно смещены.
(3) @PeterCordes обнаружил, что устранение ходов происходит в Skylake большую часть времени. Я также могу подтвердить это на Haswell.