Как точно выполняют частичные регистры на Haswell/Skylake? Написание AL, похоже, имеет ложную зависимость от RAX, а AH несовместим

Этот цикл работает на одной итерации в течение 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-break xor 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 + 6x mov 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.

Ответ 1

Другие ответы приветствуются, чтобы обратиться к Sandybridge и IvyBridge более подробно.  У меня нет доступа к этому оборудованию.


Я не обнаружил каких-либо различий между HSW и SKL.  На Haswell и Skylake все, что я тестировал до сих пор, поддерживает эту модель:

AL никогда не переименовывается отдельно от RAX (или r15b от r15). Поэтому, если вы никогда не касаетесь регистров high8 (AH/BH/CH/DH), все ведет себя точно так же, как на процессоре без переименования с частичной регистрацией (например, AMD).

Доступ только для записи к AL сливается с RAX с зависимостью от RAX. Для загрузок в AL это - микроплавленый ALU + load uop, который выполняется на p0156, что является одним из самых убедительных доказательств того, что он действительно объединяется при каждой записи, а не просто выполняет какую-то изощренную двойную бухгалтерию, как предположил Агнер.

Агнер (и Intel) говорят, что для Sandybridge может потребоваться объединенная мера для AL, поэтому он, вероятно, переименован отдельно от RAX. Для SnB в руководстве по оптимизации Intel (раздел 3.5.2.4 Частичные регистры) написано

SnB (не обязательно более поздняя версия) вставляет объединяющую меру в следующих случаях:

  • После записи в один из регистров AH, BH, CH или DH и до после чтения 2-, 4- или 8-байтовой формы того же регистра. В В этих случаях вставляется микрооперация. Вставка потребляет полный цикл распределения, в котором другие микрооперации не могут быть распределены.

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

Я думаю, они говорят, что на SnB add al,bl будет RMW полностью RAX вместо того, чтобы переименовывать его отдельно, потому что один из исходных регистров является (частью) RAX. Я предполагаю, что это не относится к такой нагрузке, как mov al, [rbx + rax]; rax в режиме адресации, вероятно, не считается источником.

Я не проверял, должны ли high8 слияния все еще самостоятельно выпускать/переименовывать в HSW/SKL. Это сделало бы фронтальный удар эквивалентным 4 мопам (с тех пор проблема/переименование ширины конвейера).

  • Нет способа разорвать зависимость с AL без написания EAX/RAX. xor al,al не помогает, как и mov al, 0.
  • movzx ebx, al имеет нулевую задержку (переименована) и не нуждается в исполнительном блоке. (т.е. работы по устранению mov на HSW и SKL). Он запускает слияние AH, если он грязный, который, я думаю, необходим для того, чтобы он работал без ALU. Вероятно, это не совпадение с тем, что Intel отказалась от переименования в том же Uarch, который ввел mov-elission. (В руководстве по микроархиву Agner Fog говорится, что ходы с нулевым расширением не исключаются в HSW или SKL, только в IvB.)
  • movzx eax, al не устраняется при переименовании. MOV-ликвидации на Intel никогда не работает на то же самое, то же самое. mov rax,rax также не устранен, хотя он не должен ничего расширять. (Хотя не было бы никакого смысла предоставлять ему специальную аппаратную поддержку, потому что это просто запрет, в отличие от mov eax,eax). В любом случае, при расширении нуля предпочитайте перемещаться между двумя отдельными архитектурными регистрами, будь то 3-битный 2- mov или 8-битный movzx.
  • movzx eax, bx не устраняется при переименовании в HSW или SKL. Он имеет задержку 1С и использует ALU UOP. В руководстве по оптимизации Intel упоминается только нулевая задержка для 8-битного movzx (и указывается, что movzx r32, high8 никогда не переименовывается).

Регистры с высокими значениями 8 могут быть переименованы отдельно от остальной части регистра, и они действительно требуют слияния.

  • Доступ только для записи к ah с помощью mov ah, reg8 или mov ah, [mem8] переименовывает AH, без зависимости от старого значения. Обе эти инструкции обычно не нуждаются в ALU-мопе для 3-битной версии 2-. (Но mov ah, bl не исключен; ему действительно необходим p0156 ALU, чтобы это могло быть совпадением).
  • RMW из AH (например, inc ah) загрязняет его.
  • setcc ah зависит от старого ah, но все равно его загрязняет. Я думаю, что mov ah, imm8 такой же, но не проверял так много angular случаев.

    (Необъяснимо: цикл, включающий setcc ah, может иногда выполняться из ЛСД, см. цикл rcr в конце этого поста. Возможно, до тех пор, пока ah чист в конце цикла, он может использовать ЛСД?).

    Если ah грязный, setcc ah сливается с переименованным ah, вместо того, чтобы форсировать слияние с rax. например %rep 4 (inc al/test ebx,ebx/setcc ah/inc al/inc ah) не генерирует мопов слияния и работает только примерно в 8,7 с (задержка 8 inc al замедляется из-за конфликтов ресурсов из-за моп для ah. Также для депо inc ah/setcc ah).

    Я думаю, что здесь происходит то, что setcc r8 всегда реализован как чтение-изменение-запись. Вероятно, Intel решила, что не стоит делать моп setcc только для записи, чтобы оптимизировать случай setcc ah, поскольку это очень редко встречается для кода, сгенерированного компилятором, для setcc ah. (Но смотрите ссылку на этот вопрос в вопросе: clang4.0 с -m32 сделает это.)

  • reading AX, EAX, or RAX triggers a merge uop (which takes up front-end issue/rename bandwidth). Probably the RAT (Register Allocation Table) tracks the high-8-dirty state for the architectural R[ABCD]X, и even after a write to AH retires, the AH data is stored in a separate physical register from RAX. Even with 256 NOPs between writing AH и reading EAX, there is an extra merging uop. (ROB size=224 on SKL, so this guarantees that the mov ah, 123 was retired). Detected with uops_issued/executed perf counters, which clearly show the difference.

  • Чтение AX, EAX или RAX запускает объединение (которое занимает внешнюю проблему/переименовывает пропускную способность). Вероятно, RAT (таблица распределения регистров) отслеживает состояние с высоким уровнем загрязнения для архитектурного R [ABCD] X, и даже после прекращения записи в AH данные AH сохраняются в отдельном физическом регистре от RAX. Даже с 256 NOP между записью AH и чтением EAX, существует дополнительный слияние. (Размер ROB = 224 в SKL, так что это гарантирует, что mov ah, 123 был удален). Обнаружено с помощью uops_issued/execute счетчиков перфорации, которые четко показывают разницу. Чтение-изменение-запись AL (например, inc al) сливается бесплатно, как часть ALU UOP. (Тестируется только с несколькими простыми мопами, такими как add/inc, но не div r8 или mul r8). Опять же, слияние не происходит, даже если AH грязный.

  • Только для записи в EAX/RAX (например, lea eax, [rsi + rcx] или xor eax,eax) очищает грязное состояние AH (без слияния uop).

  • Только запись в AX (mov ax, 1) сначала вызывает слияние AH. Я думаю, что вместо специального случая это работает как любой другой RMW AX/RAX. (TODO: тест mov ax, bx, хотя это не должно быть особенным, потому что он не переименован.)
  • xor ah,ah имеет задержку 1с, не вызывает прерывания и все еще нуждается в порте выполнения.
  • Чтение и/или запись AL не приводит к слиянию, поэтому AH может оставаться грязным (и использоваться независимо в отдельной цепочке развертывания). (например, add ah, cl/add al, dl могут работать по 1 за такт (узкое место при дополнительной задержке).

Загрязнение AH предотвращает запуск цикла из LSD (буфер цикла), даже если нет слияний. LSD - это когда процессор перезагружает мопы в очереди, которая передает этап выпуска/переименования. (Называется IDQ).

Вставка объединяющихся мопов немного похожа на вставку стековых синхронизирующих мопов для механизма стеков. В руководстве по оптимизации Intel говорится, что SnB LSD не может запускать циклы с несовпадающими push/pop, что имеет смысл, но подразумевает, что он может запускать циклы со сбалансированным push/pop. Это не то, что я вижу в SKL: даже сбалансированный push/pop предотвращает запуск с LSD (например, push rax/pop rdx/times 6 imul rax, rdx. (Может быть реальная разница между SnB LSD и HSW)/SKL: SnB может просто "заблокировать" мопы в IDQ вместо того, чтобы повторять их несколько раз, поэтому цикл из 5 мопов выдает 2 цикла вместо 1.25.) В любом случае, похоже, что HSW/SKL не может использовать LSD, когда регистр старшего разряда загрязнен или когда он содержит мопы стекового механизма.

Такое поведение может быть связано с ошибкой в SKL:

SKL150: Короткие циклы, использующие регистры AH/BH/CH/DH, могут вызвать непредсказуемое поведение системы

Проблема: В сложных микроархитектурных условиях короткие циклы из менее чем 64 команд, которые используют регистры AH, BH, CH или DH, а также соответствующие им более широкие регистры (например, RAX, EAX или AX для AH), могут вызвать непредсказуемое поведение системы., Это может произойти, только если оба логических процессора на одном физическом процессоре активны.

Это также может быть связано с инструкцией Intel по оптимизации, согласно которой SnB, по крайней мере, должен сам выпустить/переименовать операцию AH-merge в цикле. Это странная разница для внешнего интерфейса.

Мой журнал ядра Linux говорит microcode: sig=0x506e3, pf=0x2, revision=0x84. Пакет Arch Linux intel-ucode просто предоставляет обновление, вы должны отредактировать файлы конфигурации, чтобы фактически загрузить его. Поэтому мой тест Skylake проводился на i7-6700k с ревизией микрокода 0x84, которая не включает исправление для SKL150. Это соответствует поведению Хэсвелла в каждом случае, который я проверял, IIRC. (например, и Haswell, и мой SKL могут запустить цикл setne ah/add ah,ah/rcr ebx,1/mov eax,ebx из LSD). У меня включен HT (что является предварительным условием для манифеста SKL150), но я тестировал в основном простаивающей системе, поэтому мой поток имел ядро для себя.

С обновленным микрокодом LSD полностью отключен на все время, а не только когда активны частичные регистры. lsd.uops всегда точно равен нулю, в том числе для реальных программ, а не для синтетических циклов. Аппаратные ошибки (а не ошибки микрокода) часто требуют отключения целой функции для исправления. Вот почему сообщается, что у SKL-avx512 (SKX) нет буфера обратной связи. К счастью, это не проблема производительности: повышенная пропускная способность UL-кэша в SKL по сравнению с Broadwell почти всегда идет в ногу с проблемой/переименованием.


Дополнительная задержка AH/BH/CH/DH:

  • Чтение AH, когда оно не загрязнено (переименовано отдельно), добавляет дополнительный цикл задержки для обоих операндов. например add bl, ah имеет задержку 2c от входа BL до выхода BL, поэтому он может добавить задержку к критическому пути, даже если RAX и AH не являются его частью. (Я видел такой вид дополнительной задержки для другого операнда ранее, с векторной задержкой на Skylake, где задержка int/float "загрязняет" регистр навсегда. TODO: запишите это.)

Это означает, что распаковка байтов с помощью movzx ecx, al/movzx edx, ah имеет дополнительную задержку по сравнению с movzx/shr eax,8/movzx, но все же повышает пропускную способность.

  • Чтение AH, когда оно грязное, не добавляет задержки. (add ah,ah или add ah,dh/add dh,ah имеют задержку 1с на добавку). Я не проводил много испытаний, чтобы подтвердить это во многих angular случаях.

    Гипотеза: грязное значение high8 хранится в нижней части физического регистра. Чтение чистого старшего 8 требует сдвига для извлечения битов [15: 8], но чтение грязного старшего 8 может просто взять биты [7: 0] физического регистра, как при обычном считывании 8-битного регистра.

Дополнительная задержка не означает снижение пропускной способности. Эта программа может работать со скоростью 1 iter на 2 такта, даже если все инструкции add имеют задержку 2c (при чтении DH, который не изменяется).

global _start
_start:
    mov     ebp, 100000000
.loop:
    add ah, dh
    add bh, dh
    add ch, dh
    add al, dh
    add bl, dh
    add cl, dh
    add dl, dh

    dec ebp
    jnz .loop

    xor edi,edi
    mov eax,231   ; __NR_exit_group  from /usr/include/asm/unistd_64.h
    syscall       ; sys_exit_group(0)

 Performance counter stats for './testloop':

     48.943652      task-clock (msec)         #    0.997 CPUs utilized          
             1      context-switches          #    0.020 K/sec                  
             0      cpu-migrations            #    0.000 K/sec                  
             3      page-faults               #    0.061 K/sec                  
   200,314,806      cycles                    #    4.093 GHz                    
   100,024,930      branches                  # 2043.675 M/sec                  
   900,136,527      instructions              #    4.49  insn per cycle         
   800,219,617      uops_issued_any           # 16349.814 M/sec                 
   800,219,014      uops_executed_thread      # 16349.802 M/sec                 
         1,903      lsd_uops                  #    0.039 M/sec                  

   0.049107358 seconds time elapsed

Некоторые интересные тестовые циклы:

%if 1
     imul eax,eax
     mov  dh, al
     inc dh
     inc dh
     inc dh
;     add al, dl
    mov cl,dl
    movzx eax,cl
%endif

Runs at ~2.35c per iteration on both HSW and SKL.  reading 'dl' has no dep on the 'inc dh' result.  But using 'movzx eax, dl' instead of 'mov cl,dl' / 'movzx eax,cl' causes a partial-register merge, and creates a loop-carried dep chain.  (8c per iteration).


%if 1
    imul  eax, eax
    imul  eax, eax
    imul  eax, eax
    imul  eax, eax
    imul  eax, eax         ; off the critical path unless there a false dep

  %if 1
    test  ebx, ebx          ; independent of the imul results
    ;mov   ah, 123         ; dependent on RAX
    ;mov  eax,0           ; breaks the RAX dependency
    setz  ah              ; dependent on RAX
  %else
    mov   ah, bl          ; dep-breaking
  %endif

    add   ah, ah
    ;; ;inc   eax
;    sbb   eax,eax

    rcr   ebx, 1      ; dep on  add ah,ah  via CF
    mov   eax,ebx     ; clear AH-dirty

    ;; mov   [rdi], ah
    ;; movzx eax, byte [rdi]   ; clear AH-dirty, and remove dep on old value of RAX
    ;; add   ebx, eax          ; make the dep chain through AH loop-carried
%endif

Версия setcc (с %if 1) имеет задержку, переносимую циклом 20c, и запускается из LSD, даже если она имеет setcc ah и add ah,ah.

00000000004000e0 <_start.loop>:
  4000e0:       0f af c0                imul   eax,eax
  4000e3:       0f af c0                imul   eax,eax
  4000e6:       0f af c0                imul   eax,eax
  4000e9:       0f af c0                imul   eax,eax
  4000ec:       0f af c0                imul   eax,eax
  4000ef:       85 db                   test   ebx,ebx
  4000f1:       0f 94 d4                sete   ah
  4000f4:       00 e4                   add    ah,ah
  4000f6:       d1 db                   rcr    ebx,1
  4000f8:       89 d8                   mov    eax,ebx
  4000fa:       ff cd                   dec    ebp
  4000fc:       75 e2                   jne    4000e0 <_start.loop>

 Performance counter stats for './testloop' (4 runs):

       4565.851575      task-clock (msec)         #    1.000 CPUs utilized            ( +-  0.08% )
                 4      context-switches          #    0.001 K/sec                    ( +-  5.88% )
                 0      cpu-migrations            #    0.000 K/sec                  
                 3      page-faults               #    0.001 K/sec                  
    20,007,739,240      cycles                    #    4.382 GHz                      ( +-  0.00% )
     1,001,181,788      branches                  #  219.276 M/sec                    ( +-  0.00% )
    12,006,455,028      instructions              #    0.60  insn per cycle           ( +-  0.00% )
    13,009,415,501      uops_issued_any           # 2849.286 M/sec                    ( +-  0.00% )
    12,009,592,328      uops_executed_thread      # 2630.307 M/sec                    ( +-  0.00% )
    13,055,852,774      lsd_uops                  # 2859.456 M/sec                    ( +-  0.29% )

       4.565914158 seconds time elapsed                                          ( +-  0.08% )

Необъяснимо: он запускается из ЛСД, хотя и делает АХ грязным. (По крайней мере, я так думаю. TODO: попробуйте добавить некоторые инструкции, которые делают что-то с eax до того, как mov eax,ebx очистит его.)

Но с mov ah, bl он работает в 5.0c на итерацию (узкое место пропускной способности imul) на обоих HSW/SKL. (Закомментированное сохранение/перезагрузка тоже работает, но SKL имеет более быструю пересылку хранилищ, чем HSW, и это с переменной задержкой...)

 #  mov ah, bl   version
 5,009,785,393      cycles                    #    4.289 GHz                      ( +-  0.08% )
 1,000,315,930      branches                  #  856.373 M/sec                    ( +-  0.00% )
11,001,728,338      instructions              #    2.20  insn per cycle           ( +-  0.00% )
12,003,003,708      uops_issued_any           # 10275.807 M/sec                   ( +-  0.00% )
11,002,974,066      uops_executed_thread      # 9419.678 M/sec                    ( +-  0.00% )
         1,806      lsd_uops                  #    0.002 M/sec                    ( +-  3.88% )

   1.168238322 seconds time elapsed                                          ( +-  0.33% )

Обратите внимание, что он больше не запускается из ЛСД.

Ответ 2

Обновление: возможное свидетельство того, что IvyBridge по-прежнему переименовывает регистры low16/low8 отдельно от полного регистра, как Sandybridge, но в отличие от Haswell и более поздних.

Результаты InstLatX64 от SnB и IvB показывают пропускную способность 0.33c для movsx r16, r8 (как и ожидалось, movsx никогда не устраняется, и до Haswell было только 3 ALU).

Но, по-видимому, тест InstLat movsx r16, r8 ставит узкие места Haswell/Broadwell/Skylake с пропускной способностью 1с (см. также этот отчет об ошибке на instlat github). Возможно, написав тот же архитектурный регистр, создав цепочку слияний.

(Фактическая пропускная способность для этой инструкции с отдельными регистрами назначения составляет 0,25 с на моем Skylake. Протестировано с 7 movsx инструкциями, записывающими в eax..edi и r10w/r11w, все считывают из cl. И dec ebp/jnz в качестве ветвь цикла, чтобы сделать цикл равным 8 моп.)

Если я правильно догадываюсь о том, что создало тот результат пропускной способности 1с на процессорах после IvB, это делает что-то вроде запуска блока movsx dx, al. И это может работать только на более чем 1 IPC на процессорах, которые переименовывают dx отдельно от RDX вместо слияния. Таким образом, мы можем сделать вывод, что IvB на самом деле все же переименовывает регистры low8/low16 отдельно от полных регистров, и только в Haswell они их отбросили. (Но здесь что-то подозрительно: если это объяснение было правильным, мы должны увидеть ту же пропускную способность 1с на AMD, которая не переименовывает частичные регистры. Но мы не видим, см. ниже.)

Результаты с пропускной способностью ~ 0,33 с для тестов movsx r16, r8movzx r16, r8):

У Haswell таинственная пропускная способность 0.58c для movsx/zx r16, r8:

Другие более ранние и более поздние результаты Haswell (и CrystalWell)/Broadwell/Skylake имеют пропускную способность 1.0c для этих двух тестов.

  • HSW с 4.1.570.0 5 июня 2013 года, BDW с 4.3.15787.0 12 октября 2018 года, BDW с 4.3.739.0 17 марта 2017 года.

Как я сообщал в связанной проблеме InstLat на github, числа "задержки" для movzx r32, r8 игнорируют удаление mov, предположительно тестируя как movzx eax, al.

Хуже того, более новые версии InstLatX64 с версиями теста с отдельными регистрами, такие как MOVSX r1_32, r2_8, показывают значения задержки ниже 1 цикла, например 0,3c для этого MOVSX на Skylake. Это полная чушь; Я проверил просто чтобы быть уверенным.

Тест MOVSX r1_16, r2_8 действительно показывает задержку 1с, так что, очевидно, они просто измеряют задержку выходной (ложной) зависимости. (Который не существует для 32-битных и более широких выходов).

Но этот тест MOVSX r1_16, r2_8 измерял задержку в 1с на Sandybridge ! Так что, возможно, моя теория ошибочна в том, что говорит нам тест movsx r16, r8.


На Ryzen (AIDA64 build 4.3.781.0 21 февраля 2018 г.), , который, как мы знаем, вообще не выполняет переименование с частичным регистром, результаты не показывают эффект пропускной способности 1c, который мы ожидать, что тест действительно многократно записывал один и тот же 16-битный регистр. Я не нахожу его ни на каких более старых процессорах AMD, с более старыми версиями InstLatX64, такими как K10 или семейство Bulldozer.

## Instlat Zen tests of ... something?
  43 X86     :MOVSX r16, r8                L:   0.28ns=  1.0c  T:   0.11ns=  0.40c
  44 X86     :MOVSX r32, r8                L:   0.28ns=  1.0c  T:   0.07ns=  0.25c
  45 AMD64   :MOVSX r64, r8                L:   0.28ns=  1.0c  T:   0.12ns=  0.43c
  46 X86     :MOVSX r32, r16               L:   0.28ns=  1.0c  T:   0.12ns=  0.43c
  47 AMD64   :MOVSX r64, r16               L:   0.28ns=  1.0c  T:   0.13ns=  0.45c
  48 AMD64   :MOVSXD r64, r32              L:   0.28ns=  1.0c  T:   0.13ns=  0.45c

IDK, почему пропускная способность для всех них не равна 0,25; кажется странным Это может быть версия эффекта пропускной способности 0.58c Haswell. Числа MOVZX одинаковы, с пропускной способностью 0,25 для версии без префиксов, которая читает R8 и записывает R32. Может быть, узкое место при извлечении/декодировании для больших инструкций? Но movsx r32, r16 имеет тот же размер, что и movsx r32, r8.

Тесты Отдельные-рег показывают ту же схему, что и на Intel, однако, с задержкой 1с только для того, который должен объединиться. MOVZX такой же.

## Instlat Zen separate-reg tests
2252 X86     :MOVSX r1_16, r2_8            L:   0.28ns=  1.0c  T:   0.08ns=  0.28c
2253 X86     :MOVSX r1_32, r2_8            L:   0.07ns=  0.3c  T:   0.07ns=  0.25c
2254 AMD64   :MOVSX r1_64, r2_8            L:   0.07ns=  0.3c  T:   0.07ns=  0.25c
2255 X86     :MOVSX r1_32, r2_16           L:   0.07ns=  0.3c  T:   0.07ns=  0.25c

Результаты экскаватора также очень похожи на это, но, конечно, более низкая пропускная способность.

https://www.uops.info/table.html  подтверждает, что Zen+ имеет ожидаемую пропускную способность 0,25c (и задержку 1c) для MOVSX_NOREX (R16, R8), так же, как Instlat обнаружил в своих тестах с отдельной регистрацией.

Возможно, в тесте пропускной способности InstLat для MOVSX r16, r8 (не MOVSX r1_16, r2_8) используются только 2 или 3 цепочки dep, что недостаточно для современных процессоров? Или, может быть, иногда прерывает цепочку депов, так что OoO exec может перекрывать некоторые?