Почему 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 все время.

Другие вопросы по тегам