Ответ 1
Пропуск цикла в вопросе не зависит от латентности MOV или (от Haswell) преимущества использования блока исполнения.
Цикл по-прежнему остается всего 4 юпиша для выхода интерфейса в ядро вне порядка. (mov
по-прежнему нужно отслеживать ядром вне порядка, даже если ему не нужен блок исполнения, но cmp/jc
макро-предохранители в один uop).
Процессоры Intel, так как Core2 имели ширину проблемы 4 раза за такт, поэтому mov
не останавливает ее от выполнения на (близком к) одного итера за часы на Haswell. Он также работал бы на один за такт в Айвибридже (с отменой mov), но не на Sandybridge (без исключения). В SnB это будет примерно один цикл за 1.333c, ограниченный пропускной способностью ALU, потому что mov
всегда будет нужен. (SnB/IvB имеет только три порта ALU, а Haswell - четыре).
Обратите внимание, что специальная обработка на этапе переименования была для x87 FXCHG (swap st0
с st1
) намного дольше, чем MOV. Agner Fog показывает FXCHG как 0 латентность на PPro/PII/PIII (ядро первого поколения P6).
Цикл в вопросе имеет две взаимосвязанные цепи зависимостей (add edi,esi
зависит от EDI и от счетчика циклов ESI), что делает его более чувствительным к несовершенному планированию. Снижение на 2% по сравнению с теоретическим предсказанием из-за кажущихся несвязанных команд не является необычным, и небольшие изменения в порядке инструкций могут сделать такую разницу. Чтобы работать с точностью до 1 с на каждый, каждый цикл должен запускать INC и ADD. Поскольку все INC и ADD зависят от предыдущей итерации, выполнение вне порядка не может догнать, запустив два за один цикл. Хуже того, ADD зависит от INC в предыдущем цикле, что я подразумевал под "блокировкой", поэтому потеря цикла в цепочке отпечатков INC также останавливает цепочку отладки ADD.
Кроме того, предсказанные ветки могут работать только на port6, поэтому любой цикл, в котором port6 не выполняется, cmp/jc - это цикл потерянной пропускной способности. Это происходит каждый раз, когда INC или ADD крадут цикл на порте6 вместо запуска на портах 0, 1 или 5. IDK, если это преступник, или если потерять циклы в самих цепочках отпечатков INC/ADD, или, может быть, некоторые из них.
Добавление дополнительного MOV не добавляет никакого давления на порт выполнения, предполагая, что он устранен на 100%, но он не позволяет переднему концу работать перед ядром выполнения. (Только 3 из 4-х точек в цикле нуждаются в исполнительном модуле, и ваш процессор Haswell может запускать INC и ADD на любом из своих 4 портов ALU: 0, 1, 5 и 6. Таким образом, узкие места:
- максимальная пропускная способность интерфейса 4 часа в час. (Цикл без MOV - всего 3 юапа, поэтому интерфейс может работать впереди).
- пропускная способность в 1 раз за часы.
- цепочка зависимостей, включающая
esi
(латентность INC 1 за часы) - цепочка зависимостей, включающая
edi
(ADD латентность 1 за такт, а также зависит от INC от предыдущей итерации)
Без MOV интерфейсный модуль может выдавать три петли на 4 за такт до тех пор, пока ядро не будет заполнено. (AFAICT, он "разворачивает" крошечные циклы в кольцевом буфере (Loop Stream Detector: LSD), поэтому цикл с ABC-дисками может выдаваться в шаблоне ABCA BCAB CABC.... Первой счетчик для lsd.cycles_4_uops
подтверждает, что он в основном выдает группы из 4, когда он выдает какие-либо ошибки.)
Процессоры Intel присваивают удаленным портам по мере их выхода в ядро из-за порядка. Решение основано на счетчиках, которые отслеживают, сколько uops для каждого порта уже находятся в планировщике (aka Reservation Station, RS). Когда в RS ожидает много ошибок, это хорошо работает и обычно должно избегать планирования INC или ADD на порт6. И я предполагаю, что также избегает планирования INC и ADD, так что время теряется из любой из этих цепочек dep. Но если RS пуст или почти пуст, счетчики не остановят ADD или INC от кражи цикла на порту6.
Я думал, что нахожусь здесь, но любое субоптимальное планирование должно позволить фронту догнать и сохранить полный конец. Я не думаю, что мы должны ожидать, что интерфейсный модуль вызовет достаточное количество пузырьков в конвейере, чтобы объяснить падение на 2% ниже максимальной пропускной способности, поскольку крошечный цикл должен работать от буфера цикла с очень последовательной 4-х тактовой пропускной способностью. Возможно, там что-то еще происходит.
Настоящий пример преимущества устранения mov
.
Я использовал lea
для построения цикла, который имеет только один mov
за такт, создавая прекрасную демонстрацию, где MOV-устранение преуспевает на 100%, или 0% времени с помощью mov same,same
, чтобы продемонстрировать узкое место задержки, которое производит.
Так как макро-fused dec/jnz
является частью цепочки зависимостей, включающей счетчик циклов, то несовершенное планирование не может его задержать. Это отличается от случая, когда cmp/jc
"отключается" от цепочки зависимостей критического пути на каждой итерации.
_start:
mov ecx, 2000000000 ; each iteration decrements by 2, so this is 1G iters
align 16 ; really align 32 makes more sense in case the uop-cache comes into play, but alignment is actually irrelevant for loops that fit in the loop buffer.
.loop:
mov eax, ecx
lea ecx, [rax-1] ; we vary these two instructions
dec ecx ; dec/jnz macro-fuses into one uop in the decoders, on Intel
jnz .loop
.end:
xor edi,edi ; edi=0
mov eax,231 ; __NR_exit_group from /usr/include/asm/unistd_64.h
syscall ; sys_exit_group(0)
В семействе Intel SnB LEA с одним или двумя компонентами в режиме адресации работает с задержкой 1 с (см. http://agner.org/optimize/ и другие ссылки в x86 tag wiki).
Я построил и запускал это как статический бинарный файл в Linux, поэтому перманентные счетчики пользовательского пространства для всего процесса измеряют только цикл с незначительными затратами на запуск/завершение работы. (perf stat
действительно легко по сравнению с тем, чтобы поставить персистентные запросы в саму программу)
$ yasm -felf64 -Worphan-labels -gdwarf2 mov-elimination.asm && ld -o mov-elimination mov-elimination.o &&
objdump -Mintel -drwC mov-elimination &&
taskset -c 1 ocperf.py stat -etask-clock,context-switches,page-faults,cycles,instructions,branches,uops_issued.any,uops_executed.thread -r2 ./mov-elimination
Disassembly of section .text:
00000000004000b0 <_start>:
4000b0: b9 00 94 35 77 mov ecx,0x77359400
4000b5: 66 66 2e 0f 1f 84 00 00 00 00 00 data16 nop WORD PTR cs:[rax+rax*1+0x0]
00000000004000c0 <_start.loop>:
4000c0: 89 c8 mov eax,ecx
4000c2: 8d 48 ff lea ecx,[rax-0x1]
4000c5: ff c9 dec ecx
4000c7: 75 f7 jne 4000c0 <_start.loop>
00000000004000c9 <_start.end>:
4000c9: 31 ff xor edi,edi
4000cb: b8 e7 00 00 00 mov eax,0xe7
4000d0: 0f 05 syscall
perf stat -etask-clock,context-switches,page-faults,cycles,instructions,branches,cpu/event=0xe,umask=0x1,name=uops_issued_any/,cpu/event=0xb1,umask=0x1,name=uops_executed_thread/ -r2 ./mov-elimination
Performance counter stats for './mov-elimination' (2 runs):
513.242841 task-clock:u (msec) # 1.000 CPUs utilized ( +- 0.05% )
0 context-switches:u # 0.000 K/sec
1 page-faults:u # 0.002 K/sec
2,000,111,934 cycles:u # 3.897 GHz ( +- 0.00% )
4,000,000,161 instructions:u # 2.00 insn per cycle ( +- 0.00% )
1,000,000,157 branches:u # 1948.396 M/sec ( +- 0.00% )
3,000,058,589 uops_issued_any:u # 5845.300 M/sec ( +- 0.00% )
2,000,037,900 uops_executed_thread:u # 3896.865 M/sec ( +- 0.00% )
0.513402352 seconds time elapsed ( +- 0.05% )
Как и ожидалось, цикл работает 1G раз (branches
~ = 1 миллиард). "Дополнительные" 111k циклов за пределами 2G - это накладные расходы, которые присутствуют и в других тестах, включая тот, у которого нет mov
. Это не из-за случайного отказа от исключения mov, но он масштабируется с подсчетом итераций, поэтому он не просто накладные расходы на запуск. Вероятно, это связано с прерываниями таймера, поскольку IIRC Linux perf
не вмешивается в работу с первыми счетчиками при обработке прерываний и просто позволяет им вести учет. (perf
виртуализирует счетчики производительности оборудования, чтобы вы могли получать подсчеты на каждый процесс, даже когда поток переносится через центральные процессоры.) Кроме того, прерывания таймера в логическом ядре дочернего узла, которые используют одно и то же физическое ядро, будут немного беспокоить.
Узким местом является цепочка зависимостей, связанная с циклом, включающая счетчик циклов. Циклы 2G для 1G-итераторов - 2 такта на итерацию или 1 такт на декрет. Подтверждает, что длина цепочки dep составляет 2 цикла. Это возможно, только если mov
имеет нулевую задержку. (Я знаю, что это не доказывает, что нет какого-то другого узкого места.Это действительно только доказывает, что латентность не более двух циклов, если вы не верите моему утверждению, что латентность является единственным узким местом. Там resource_stalls.any
perf counter, но у него не так много вариантов для разрушения, из которого был исчерпан ресурс микроархитектуры.)
В цикле есть 3-х платных доменов: mov
, lea
и macro-fused dec/jnz
. Счетчик 3G uops_issued.any
подтверждает, что: он рассчитывается в плавленном домене, который является всем конвейером от декодеров до выхода на пенсию, за исключением планировщика (RS) и исполнительных блоков. (пары с макросплавными ключами остаются как единичные uop везде. Это только для микро-слияния магазинов или ALU + нагрузки, что 1 fused-domain uop в ROB отслеживает прогресс двух непроверенных доменов.)
2G uops_executed.thread
(нераспределенный домен) сообщает нам, что все mov
uops были устранены (т.е. обрабатываются этапом выпуска/переименования и помещены в ROB в уже выполненном состоянии). Они по-прежнему занимают пропускную способность для выхода/выхода на пенсию, а также пространство в кэше uop и размер кода. Они занимают место в ROB, ограничивая размер окна вне порядка. A mov
инструкция никогда не бывает свободной. Существует множество возможных узловых мест микроархитектуры, помимо латентных и рабочих портов, наиболее важным из которых обычно является 4-х уровневая проблема с интерфейсом.
В процессорах Intel отсутствие нулевой задержки часто является крупной сделкой, чем не требуется исполнительный блок, особенно в Haswell, а затем, где есть 4 порта ALU. (Но только 3 из них могут обрабатывать векторные вершины, поэтому не устраненные движения векторов будут более узким местом, особенно в коде без большого количества загрузок или хранилищ с пропускной способностью переднего плана (4 сокета с плавным доменом на каждый такт) от ALU uops Кроме того, планирование uops для исполнительных блоков не является совершенным (более похоже на старейшее в начале), поэтому uops, которые не находятся на критическом пути, могут красть циклы с критического пути.)
Если мы поместим в цикл nop
или xor edx,edx
, они также будут выпущены, но не будут выполняться на процессорах Intel SnB.
Модификация исключения с нулевой задержкой может быть полезна для нулевого расширения от 32 до 64 бит и от 8 до 64. ( movzx eax, bl
исключается, movzx eax, bx
isn ' т).
Без mov-elim
mov ecx, ecx # Intel can't eliminate mov same,same
lea ecx, [rcx-1]
dec ecx
jnz .loop
3,000,320,972 cycles:u # 3.898 GHz ( +- 0.00% )
4,000,000,238 instructions:u # 1.33 insn per cycle ( +- 0.00% )
1,000,000,234 branches:u # 1299.225 M/sec ( +- 0.00% )
3,000,084,446 uops_issued_any:u # 3897.783 M/sec ( +- 0.00% )
3,000,058,661 uops_executed_thread:u # 3897.750 M/sec ( +- 0.00% )
Это занимает 3G-циклы для итераций 1G, потому что длина цепочки зависимостей теперь составляет 3 цикла.
Счетчик uop с плавленной областью не изменился, все еще 3G.
Что изменилось, так это то, что теперь счетчик uop незадействованных доменов совпадает с объединенным доменом. Для всех юотов нужен исполнительный блок; ни одна из инструкций mov
не была устранена, поэтому все они добавили латентность 1c к цепочке отрезков, связанной с циклом.
(Когда есть микро-fused uops, например add eax, [rsi]
, счетчик uops_executed
может быть выше, чем uops_issued
. Но у нас этого нет.)
Без mov
:
lea ecx, [rcx-1]
dec ecx
jnz .loop
2,000,131,323 cycles:u # 3.896 GHz ( +- 0.00% )
3,000,000,161 instructions:u # 1.50 insn per cycle
1,000,000,157 branches:u # 1947.876 M/sec
2,000,055,428 uops_issued_any:u # 3895.859 M/sec ( +- 0.00% )
2,000,039,061 uops_executed_thread:u # 3895.828 M/sec ( +- 0.00% )
Теперь мы возвращаемся к задержке в 2 цикла для цепочки отрезков с циклом.
Ничего не устранено.
Я тестировал сканер Skylake на 3,9 ГГц i7-6700k. Я получаю одинаковые результаты на Haswell i5-4210U (с точностью до 40 тыс. Из 1 Г) для всех перфомансов. Это примерно такая же ошибка, как и повторная работа в той же системе.
Обратите внимание, что если я запустил perf
как root и считал cycles
вместо cycles:u
(только для пользовательского пространства), он измеряет частоту процессора как точно 3.900 ГГц. (IDK почему Linux только подчиняется bios-настройкам для max turbo сразу после перезагрузки, но затем падает до 3,9 ГГц, если я оставлю его бездействующим в течение пары минут. Asus Z170 Pro Gaming mobo, Arch Linux с ядром 4.10.11-1-ARCH. Пила то же самое с Ubuntu. Запись balance_performance
в каждый из /sys/devices/system/cpu/cpufreq/policy[0-9]*/energy_performance_preference
из /etc/rc.local
исправляет его, но запись balance_power
заставляет его снова вернуться к 3.9GHz позже.)
Вы должны получить те же результаты в AMD Ryzen, так как он может устранить целое число mov
. Семейство AMD Bulldozer может только уничтожить копии регистра xmm. (Согласно Agner Fog, ymm
регистровые копии - это исключенная нижняя половина и ALU op для верхней половины.)
Например, AMD Bulldozer и Intel Ivybridge могут поддерживать пропускную способность 1 за такт для
movaps xmm0, xmm1
movaps xmm2, xmm3
movaps xmm4, xmm5
dec
jnz .loop
Но Intel Sandybridge не может устранить ходы, так что это будет узким местом на 4 ALU uops для 3 рабочих портов. Если вместо movaps было pxor xmm0,xmm0
, SnB также мог поддерживать одну итерацию за такт. (Но семейство Bulldozer не могло, потому что xor-zeroing все еще нуждается в исполнительном модуле на AMD, даже несмотря на то, что он не зависит от старого значения регистра. И семейство Bulldozer имеет только 0,5c пропускную способность для PXOR.)
Ограничения исключения mov
Две зависимые команды MOV в строке раскрывают разницу между Haswell и Skylake.
.loop:
mov eax, ecx
mov ecx, eax
sub ecx, 2
jnz .loop
Haswell: незначительная переменная run-to-run (от 1,746 до 1,749 c/iter), но это типично:
1,749,102,925 cycles:u # 2.690 GHz
4,000,000,212 instructions:u # 2.29 insn per cycle
1,000,000,208 branches:u # 1538.062 M/sec
3,000,079,561 uops_issued_any:u # 4614.308 M/sec
1,746,698,502 uops_executed_core:u # 2686.531 M/sec
745,676,067 lsd_cycles_4_uops:u # 1146.896 M/sec
Не все инструкции MOV устранены: около 0,75 из 2 на итерацию используется порт выполнения. Каждый MOV, который выполняется вместо того, чтобы быть устраненным, добавляет 1c латентности к цепочке отрезка цикла, так что это не совпадение, что uops_executed
и cycles
очень похожи. Все вершины являются частью одной цепочки зависимостей, поэтому нет возможности parallelism. cycles
всегда примерно на 5M выше, чем uops_executed
, независимо от изменения run-to-run, поэтому я предполагаю, что в другом месте используется только 5M циклов.
Skylake: более стабильны, чем результаты HSW, и больше исключений mov: всего 0,6666 MOV каждого 2 требуется исполнительный блок.
1,666,716,605 cycles:u # 3.897 GHz
4,000,000,136 instructions:u # 2.40 insn per cycle
1,000,000,132 branches:u # 2338.050 M/sec
3,000,059,008 uops_issued_any:u # 7014.288 M/sec
1,666,548,206 uops_executed_thread:u # 3896.473 M/sec
666,683,358 lsd_cycles_4_uops:u # 1558.739 M/sec
В Haswell, lsd.cycles_4_uops
учтены все ошибки. (0,745 * 4 ~ = 3). Таким образом, почти в каждом цикле, в котором выпущены любые uops, выдается полная группа из 4 (из буфера цикла. Вероятно, я должен был посмотреть на другой счетчик, который не заботится о том, откуда они пришли, например uops_issued.stall_cycles
для подсчета циклов, где не было выпущено uops).
Но на SKL, 0.66666 * 4 = 2.66664
меньше 3, поэтому в некоторых циклах на интерфейсе было выпущено менее 4 часов. (Обычно он останавливается до тех пор, пока в ядре вне очереди не будет выпущена полная группа из 4, вместо того, чтобы выпускать неполные группы).
Странно, IDK, что такое точное микроархитектурное ограничение. Поскольку цикл равен всего 3 мкп, каждая группа проблем из 4-х компьютеров - это больше, чем полная итерация. Таким образом, группа вопросов может содержать до 3 зависимых MOV. Возможно, Skylake спроектирован таким образом, чтобы иногда разрывать его, чтобы позволить больше исключать mov?
update: на самом деле это нормально для 3-юп-петель на Skylake. uops_issued.stall_cycles
показывает, что HSW и SKL выдают простой цикл 3 uop без исключения mov так же, как они выдают этот. Таким образом, лучшее устранение mov - побочный эффект разделения групп проблем по какой-либо другой причине. (Это не узкое место, потому что принятые ветки не могут выполняться быстрее, чем 1 за такт, независимо от того, насколько быстро они выдают). Я до сих пор не знаю, почему SKL отличается, но я не думаю, что об этом можно было бы беспокоиться.
В менее экстремальном случае SKL и HSW одинаковы: оба не смогли исключить 0.3333 из каждых двух инструкций MOV:
.loop:
mov eax, ecx
dec eax
mov ecx, eax
sub ecx, 1
jnz .loop
2,333,434,710 cycles:u # 3.897 GHz
5,000,000,185 instructions:u # 2.14 insn per cycle
1,000,000,181 branches:u # 1669.905 M/sec
4,000,061,152 uops_issued_any:u # 6679.720 M/sec
2,333,374,781 uops_executed_thread:u # 3896.513 M/sec
1,000,000,942 lsd_cycles_4_uops:u # 1669.906 M/sec
Все проблемы с uops в группах из 4. Любая непрерывная группа из 4-х совпадений будет содержать ровно два MOV-uops, которые являются кандидатами для исключения. Поскольку он явно преуспевает в устранении как в некоторых циклах, так и в IDK, почему он не всегда может это сделать.
Руководство по оптимизации Intel говорит о том, что перезапись результата mov-elimication как можно скорее освобождает микроархитектурный ресурсов, чтобы он мог преуспеть чаще, по крайней мере, для movzx
. См. Пример 3-25. Последовательность повторного упорядочения для повышения эффективности инструкций MOV с нулевой задержкой.
Так может быть, он отслеживается внутри с таблицей с ограниченным размером ссылок? Что-то должно остановить регистрацию файла физического регистра, если он больше не нужен в качестве значения исходного архитектурного регистра, если он по-прежнему необходим в качестве значения места назначения mov. Освобождение записей PRF как можно скорее является ключевым, потому что Размер PRF может ограничить окно вне порядка меньшим размера ROB.
Я попробовал примеры на Haswell и Skylake, и обнаружил, что удаление mov действительно на самом деле работает значительно больше времени, когда это делается, но что это было фактически немного медленнее в общих циклах, а не быстрее. Пример должен был показать преимущества IvyBridge, которые, вероятно, являются узкими местами на его 3 портах ALU, но HSW/SKL только узкое место в конфликтах ресурсов в цепочках dep и, похоже, не беспокоит необходимость использования порта ALU для большей части movzx
.
См. также Почему XCHG reg, зарегистрируйте 3 инструкции по микрооперациям на современных архитектурах Intel? для получения дополнительных исследований + догадки о том, как работает удаление ссылок, и он может работать для xchg eax, ecx
. (На практике xchg reg,reg
- это 3 ALU uops на Intel, но 2 устранены uops на Ryzen. Интересно догадаться, мог ли Intel реализовать его более эффективно.)
Кстати, в качестве обходного пути для хаусавелла Linux не предоставляет uops_executed.thread
, когда включена гиперпоточность, только uops_executed.core
. Другое ядро было абсолютно бездействующим все время, даже прерывания таймера, потому что я взял его в автономном режиме с помощью echo 0 > /sys/devices/system/cpu/cpu3/online
. К сожалению, это невозможно сделать до того, как perf
решит, что HT включен, а у моего ноутбука Dell нет опции BIOS для отключения HT. Поэтому я не могу заставить perf
использовать все 8 аппаратных счетчиков PMU сразу в этой системе, только 4.:/