Этот цикл работает на одной итерации в течение 3 циклов на Intel Conroe/Merom, с узким местом по пропускной способности imul
, как ожидалось. Но на Haswell/Skylake он работает на одной итерации за 11 циклов, по-видимому, потому, что setnz al
имеет зависимость от последнего imul
.
; synthetic micro-benchmark to test partial-register renaming
mov ecx, 1000000000
.loop: ; do{
imul eax, eax ; a dep chain with high latency but also high throughput
imul eax, eax
imul eax, eax
dec ecx ; set ZF, independent of old ZF. (Use sub ecx,1 on Silvermont/KNL or P4)
setnz al ; ****** Does this depend on RAX as well as ZF?
movzx eax, al
jnz .loop ; }while(ecx);
Если setnz al
зависит от rax
, последовательность 3ximul/setcc/movzx формирует цепочку зависимостей, связанных с циклом. Если нет, каждая цепочка setcc
/movzx
/3x imul
независима, отбрасывается из dec
, которая обновляет счетчик циклов. 11c на итерацию, измеренную на HSW/SKL, отлично объясняется узким местом задержки: 3x3c (imul) + 1c (read-modify-write by setcc) + 1c (movzx в одном регистре).
Отключить тему: избегать этих (преднамеренных) узких мест
Я собирался понять понятное/предсказуемое поведение, чтобы изолировать материал с частичным реестром, а не оптимальную производительность.
Например, xor
-zero/set-flags/ setcc
лучше в любом случае (в этом случае xor eax,eax
/dec ecx
/setnz al
). Это разбивает dep на eax на всех процессорах (кроме раннего семейства P6, таких как PII и PIII), по-прежнему избегает слияния с частичным регистром и сохраняет 1c задержки movzx
. Он также использует еще один ALU uop для процессоров, которые обрабатывают xor-zeroing в стадии переименования регистра. См. Эту ссылку для получения дополнительной информации об использовании xor-zeroing с помощью setcc
.
Обратите внимание, что AMD, Intel Silvermont/KNL и P4 вообще не выполняют переименование частичных регистров. Это только функция процессоров семейства Intel P6 и ее потомок семейства Intel Sandybridge, но, похоже, постепенно прекращается.
gcc, к сожалению, имеет тенденцию использовать cmp
/setcc al
/movzx eax,al
, где он мог бы использовать xor
вместо movzx
(Godbolt пример компилятора-проводника), в то время как clang использует xor-zero/cmp/setcc, если вы не объединяете несколько логических условий, таких как count += (a==b) | (a==~b)
.
Версия xor/dec/setnz работает на уровне 3.0c за итерацию на Skylake, Haswell и Core2 (узкое место при пропускной способности imul
). xor
-zeroing нарушает зависимость от старого значения eax
от всех процессоров вне порядка, отличных от PPro/PII/PIII/early-Pentium-M (где он по-прежнему избегает штрафов за частичную регистрацию, но не "t сломать dep). Руководство по микроаргуту Agner Fog описывает это. Замена xor-zeroing на mov eax,0
замедляет его до одного на 4.78 циклов на Core2: 2-3c stall (в интерфейсе?) Для вставки слияния с частичным списанием, когда imul
читает eax
после setnz al
.
Кроме того, я использовал movzx eax, al
, который побеждает исключение mov, как это делает mov rax,rax
. (IvB, HSW и SKL могут переименовать movzx eax, bl
с 0 латентностью, но Core2 не может). Это делает все равным по Core2/SKL, за исключением поведения с частичным регистром.
Поведение Core2 совместимо с Agar Fog microarch guide, но поведение HSW/SKL - нет. Из раздела 11.10 для Skylake, а также для предыдущих Intel uarches:
Различные части регистра общего назначения могут храниться в разных временных регистрах, чтобы удалить ложные зависимости.
Он, к сожалению, не успевает провести детальное тестирование для каждого нового uarch, чтобы перепроверить предположения, поэтому это изменение в поведении проскользнуло через трещины.
Агнер описывает, как встраиваемый uop вставлен (без остановки) для регистров high8 (AH/BH/CH/DH) на Sandybridge через Skylake, а для low8/low16 - на SnB. (Я, к сожалению, распространял неверную информацию в прошлом и говорил, что Хасуэлл может объединить AH бесплатно. Я быстро снял раздел Агнера Хасуэлла и не заметил более поздний абзац о регистрах high8. Сообщите мне, видите ли вы мои неправильные комментарии к другим сообщениям, поэтому я могу удалить их или добавить исправление. Попытаюсь хотя бы найти и отредактировать свои ответы, где я это сказал.)
Мои актуальные вопросы: Как точно ведут себя частичные регистры на Skylake?
Все ли одно и то же от IvyBridge до Skylake, включая дополнительную задержку high8?
Руководство по оптимизации Intel не является специфическим, о том, какие ЦП имеют ложные зависимости для чего (хотя он упоминает, что у некоторых ЦП есть), и оставляет такие как чтение AH/BH/CH/DH (регистры high8), добавляющие дополнительную задержку, даже если они не были изменены.
Если есть какое-либо поведение P6-семейства (Core2/Nehalem), которое руководство по микроархиву Agner Fog не описывает, это тоже было бы интересно, но я, вероятно, должен ограничивать сферу применения этого вопроса только для семейства Skylake или Sandybridge.
Мои данные теста Skylake, от размещения коротких последовательностей %rep 4
внутри небольшого цикла dec ebp/jnz
, который запускает итерации 100M или 1G. Я измерил циклы с Linux perf
так же, как в моем ответе здесь, на том же оборудовании (рабочий стол Skylake i7 6700k).
Если не указано иное, каждая команда выполняется как 1 uop с плавным доменом, используя порт выполнения ALU. (Измерено с помощью ocperf.py stat -e ...,uops_issued.any,uops_executed.thread
). Это определяет (отсутствие) отмену mov и добавление дополнительных сходов.
Случаи "4 за цикл" - это экстраполяция в бесконечно разведенный случай. Накладные расходы Loop занимают часть пропускной способности интерфейса, но все, что лучше, чем 1 за цикл, является признаком того, что переименование регистра избегало зависимости от записи после записи, и что uop не обрабатывается внутренне как чтение-изменение-запись.
Запись только в AH: предотвращает выполнение цикла из буфера loopback (также известный как Loop Stream Detector (LSD)). Подсчеты для lsd.uops
равны 0 на HSW и крошечные на SKL (около 1,8 тыс.) И не масштабируются с учетом итерации цикла. Вероятно, эти подсчеты взяты из некоторого кода ядра. Когда петли выполняются из LSD, lsd.uops ~= uops_issued
с точностью до шума измерения. Некоторые циклы чередуются между LSD или no-LSD (например, когда они могут не вписываться в кеш uop, если декодирование начинается не в том месте), но я не сталкивался с этим при тестировании этого.
- повторный
mov ah, bh
и/илиmov ah, bl
работает с 4 за цикл. Он принимает ALU uop, поэтому он не исключается, какmov eax, ebx
. - repeat
mov ah, [rsi]
работает с 2 за цикл (узкое место в пропускной способности). - repeat
mov ah, 123
работает с 1 за цикл. (A dep-breakxor eax,eax
внутри цикла удаляет узкое место.) -
повторение
setz ah
илиsetc ah
выполняется с 1 за цикл. (Отрывная разбивкаxor eax,eax
позволяет ей узкое место на пропускной способности p06 дляsetcc
и ветки цикла.)Почему запись
ah
с инструкцией, которая обычно использует блок выполнения ALU, имеет ложную зависимость от старого значения, тогда какmov r8, r/m8
не работает (для reg или memory src)? (А как насчетmov r/m8, r8
? Конечно, неважно, какой из двух кодов операций, которые вы используете для регр-рег?) -
Повторяется
add ah, 123
работает на 1 за цикл, как и ожидалось. - repeat
add dh, cl
работает с 1 за цикл. - repeat
add dh, dh
работает с 1 за цикл. - повторный
add dh, ch
работает на 0,5 за цикл. Чтение [ABCD] H является особенным, когда они "чисты" (в этом случае RCX совсем недавно не изменен).
Терминология: все они оставляют AH (или DH) " грязными", т.е. нуждаются в слиянии (с объединением uop), когда остальная часть регистра читать (или в некоторых других случаях). то есть, что AH переименовывается отдельно от RAX, если я правильно понимаю это. " чистый" - это наоборот. Существует много способов очистки грязного регистра, самым простым из которых является inc eax
или mov eax, esi
.
Запись только в AL: эти циклы выполняются из LSD: uops_issue.any
~ = lsd.uops
.
- repeat
mov al, bl
работает с 1 за цикл. Случайная дефрагментацияxor eax,eax
на группу позволяет узкому месту выполнения программы на пропускную способность uop, а не задержка. - repeat
mov al, [rsi]
работает с 1 за цикл, в качестве микровыплавленного ALU + load uop. (uops_issued = 4G + накладные расходы цикла, uops_executed = 8G + накладные расходы цикла). Отрывная разбивкаxor eax,eax
перед группой из 4 препятствует ей узкое место при 2 нагрузках за такт. - repeat
mov al, 123
работает с 1 за цикл. - repeat
mov al, bh
работает со скоростью 0,5 за цикл. (1 на 2 цикла). Чтение [ABCD] H является особенным. -
xor eax,eax
+ 6xmov al,bh
+dec ebp/jnz
: 2c за истребитель, узкое место на 4 часа в час для интерфейса. - repeat
add dl, ch
работает со скоростью 0,5 за цикл. (1 на 2 цикла). Чтение [ABCD] H, по-видимому, создает дополнительную задержку дляdl
. - repeat
add dl, cl
работает от 1 за цикл.
Я думаю, что запись в low-8 reg ведет себя как RMW-смесь в полный рег, например, add eax, 123
будет, но не вызывает слияние, если ah
грязно. Таким образом (кроме игнорирования слияния ah
) он ведет себя так же, как и на процессорах, которые вообще не переименовывают частичное переименование. Кажется, что AL
никогда не переименовывается отдельно от rax
?
-
inc al
/inc ah
пары могут работать параллельно. -
mov ecx, eax
вставляет слияние uop, еслиah
"грязный", но фактическийmov
переименован. Это то, что Agner Fog описывает для IvyBridge и позже. - Повторяется
movzx eax, ah
выполняется один раз в 2 цикла. (Чтение регистров высокого уровня после записи полных регистров имеет дополнительную задержку.) -
movzx ecx, al
имеет нулевую задержку и не принимает порт выполнения на HSW и SKL. (Как то, что описывает Agner Fog для IvyBridge, но он говорит, что HSW не переименовывает movzx). -
movzx ecx, cl
имеет задержку 1 с и принимает порт выполнения. (mov-elim никогда не работает для случаяsame,same
, только между различными архитектурными регистрами.)Цикл, который вставляет слияние uop, каждая итерация не может выполняться из LSD (буфер цикла)?
Я не думаю, что в AL/AH/RAX есть что-то особенное против B *, C *, DL/DH/RDX. Я тестировал некоторые частичные регистры в других регистрах (хотя я обычно показываю AL
/ah
для согласованности) и никогда не замечал никакой разницы.
Как мы можем объяснить все эти наблюдения разумной моделью того, как микроарх работает внутри?
Связано: проблемы с Partial отличаются от частичных register проблем. См. Инструкция INC против ADD 1: имеет ли это значение? для некоторых супер-странных вещей с shr r32,cl
(и даже shr r32,2
на Core2/Nehalem: не читайте флаги смены, отличные от 1).
См. также Проблемы с ADC/SBB и INC/DEC в жестких циклах на некоторых процессорах для элементов с частичным флагом в циклах adc
.