Почему jnz не считает цикл?
В интернет-ресурсе я обнаружил, что у IvyBridge есть 3 ALU. Поэтому я пишу небольшую программу для тестирования:
global _start
_start:
mov rcx, 10000000
.for_loop: ; do {
inc rax
inc rbx
dec rcx
jnz .for_loop ; } while (--rcx)
xor rdi, rdi
mov rax, 60 ; _exit(0)
syscall
Я компилирую и запускаю perf
:
$ nasm -felf64 cycle.asm && ld cycle.o && sudo perf stat ./a.out
Вывод показывает:
10,491,664 cycles
что на первый взгляд имеет смысл, потому что есть 3 независимых инструкции (2 inc
и 1 dec
), который использует ALU в цикле, поэтому они считают 1 цикл вместе.
Но я не понимаю, почему весь цикл имеет только 1 цикл? jnz
зависит от результата dec rcx
, он должен считать 1 цикл, так что весь цикл составляет 2 цикла. Я ожидаю, что результат будет близок к 20,000,000 cycles
,
Я также пытался изменить второй inc
от inc rbx
в inc rax
что делает его зависимым от первого inc
, Результат становится близким к 20,000,000 cycles
, который показывает, что зависимость будет задерживать инструкцию, чтобы они не могли выполняться одновременно. Так почему jnz
особенный?
Что мне здесь не хватает?
1 ответ
Прежде всего, dec/jnz
будет объединяться в одну семью на семействе Intel Sandybridge. Вы можете победить это, поместив не устанавливающую флаг инструкцию между dec и jnz.
.for_loop: ; do {
inc rax
dec rcx
lea rbx, [rbx+1] ; doesn't touch flags, defeats macro-fusion
jnz .for_loop ; } while (--rcx)
Это все равно будет работать на 1 итере за цикл в Haswell, а затем и в Ryzen, потому что у них есть 4 целочисленных порта выполнения, чтобы не отставать от 4 мопов за итерацию. (Ваш цикл с макро-слиянием - это всего 3 мопа слитых доменов на процессорах Intel, поэтому SnB/IvB также может запускать его по 1 за такт.)
См . Руководство по оптимизации Агнера Фога и особенно его руководство по микроархам. Также другие ссылки в https://stackru.com/tags/x86/info.
Управляющие зависимости скрыты предсказанием ветвлений + умозрительным выполнением, в отличие от зависимостей данных.
Внеочередное выполнение и прогноз ветвления + спекулятивное выполнение скрывают "задержку" управляющей зависимости. то есть следующая итерация может начаться до того, как процессор подтвердит, что jnz
должно быть действительно принято
Так что каждый jnz
имеет входную зависимость от предыдущего dec rcx
прежде чем он сможет проверить прогноз, но более поздним инструкциям не нужно ждать его проверки, прежде чем они смогут выполнить. Порядок выхода на пенсию гарантирует, что неправильная спекуляция обнаруживается до того, как что-либо может "увидеть", что это происходит (за исключением микроархитектурных эффектов, приводящих к атаке Призрака...)
10М итераций это не много. Я обычно использовал бы по крайней мере 100M для чего-то, что работает только в 1c на iter. Простой прогон микробенчмарка от 0,1 до 1 секунды обычно хорош для получения очень высокой точности и скрытия накладных расходов при запуске.
И кстати, вам не нужно sudo perf
если вы установите kernel.perf_event_paranoid = 0
с sysctl. Это почти наверняка лучше сделать, чем использовать sudo
все время.