Почему 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 особенный?

Что мне здесь не хватает?


person user10865622    schedule 04.01.2019    source источник
comment
Возможно, что-то связано с объединением инструкций.   -  person David Wohlferd    schedule 04.01.2019
comment
CMP / TEST / ADD / SUB / INC / DEC / И может объединяться с Jcc в одна макрооперация   -  person phuclv    schedule 04.01.2019
comment
Возможно, вы захотите поиграть с iaca (обсуждается здесь). Это позволяет вам визуализировать некоторые из этих вещей.   -  person David Wohlferd    schedule 04.01.2019
comment
обратите внимание, что xor edi, edi будет лучше, чем xor rdi, rdi, поскольку он короче   -  person phuclv    schedule 07.01.2019


Ответы (1)


Прежде всего, dec/jnz объединит макрос в единый uop в семействе 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://stackoverflow.com/tags/x86/info.


Управляющие зависимости скрыты предсказанием ветвлений + спекулятивным исполнением, в отличие от зависимостей данных.

Выполнение вне очереди и прогнозирование переходов + спекулятивное выполнение скрывают "задержку" зависимости управления. т.е. следующая итерация может начаться до того, как ЦП проверит, что jnz действительно нужно выполнять.

Таким образом, каждый jnz имеет входную зависимость от предыдущего dec rcx, прежде чем он сможет проверить прогноз, но более поздним инструкциям не нужно ждать его проверки, прежде чем они смогут выполнить. По порядку выход на пенсию гарантирует, что неверные предположения будут обнаружены до того, как что-либо сможет "увидеть" это (за исключением микроархитектурных эффектов, ведущих к атаке Spectre ...)


10 миллионов итераций - это не много. Обычно я использую не менее 100 МБ для чего-то, что работает всего за 1 цент за штуку. Выполнение простого микробенчмарка продолжительностью от 0,1 до 1 секунды обычно позволяет получить очень высокую точность и скрыть накладные расходы на запуск.

И, кстати, вам не понадобится sudo perf, если вы установите kernel.perf_event_paranoid = 0 с помощью sysctl. Это почти наверняка лучше, чем использовать sudo все время.

person Peter Cordes    schedule 04.01.2019
comment
Я уже убедился комментаторами, что причина кроется в макросреде. Но ваш ответ меня сильно смущает, поскольку вы, кажется, говорите, что jnz будет выполняться в том же цикле даже без макро-слияния. Я тестировал, вставляя mov rsi, rdi перед jnz, чтобы предотвратить слияние, результат становится 2 цикла на каждый. Но если я вставлю mov rsi, rdi перед dec rcx, результат будет 1 цикл на каждый. Так что, похоже, причиной является макрослияние. - person user10865622; 04.01.2019
comment
Если предсказание ветвления будет выполняться jnz независимо от слияния, то как объяснить этот результат? - person user10865622; 04.01.2019
comment
Я думаю, что понял после прочтения некоторых разделов книги Гнева. Когда я вставляю mov rsi, rdi перед jnz, слияние макросов предотвращается. Таким образом, у меня получается 3 мупа для ALU, но jnz также является мопом для ветки, который находится на порте 5 в IvyBridge, а порт 5 также является ALU. Таким образом, эти 4 операции не могут выполняться одновременно. Я правильно понял? (хотя я до сих пор не знаю, почему он становится 2c / iter, возможно, мне стоит задать новый вопрос для этого) - person user10865622; 04.01.2019
comment
@ user10865622 jnz будет выполняться в том же цикле даже без макро-слияния на Haswell и более поздних версиях, а также на Ryzen, потому что на этих микроархитектурах больше портов. Первые три инструкции вместе с jnz из предыдущей итерации могут быть отправлены в одном цикле и завершить выполнение за один цикл. - person Hadi Brais; 04.01.2019
comment
@ user10865622: добавление еще одного uop и предотвращение микро-слияния дает вам 5 мопов слияния домена. Если вы используете IvB, то у вас есть узкое место во внешнем интерфейсе, и это дубликат Снижается ли производительность при выполнении циклов, количество мопов которых не является кратной ширине процессора?. Это означает, что ваш цикл может выполняться только на одной итерации за 2 такта, потому что SnB / IvB не разворачивают циклы внутри буфера цикла: взятая ветвь цикла должна быть последней точкой группы задач (до 4 мопов ). - person Peter Cordes; 04.01.2019
comment
@PeterCordes Я не думал о том, что интерфейс является узким местом, теперь я могу объяснить счетчик циклов как для моего теста, так и для примера lea в вашем ответе. Большое спасибо! Быстрый вопрос: означает ли взятая ветка инструкцию ветки типа jnz? Я сталкивался с этим много раз, но мне не удалось найти определение взятой ветки от Google. - person user10865622; 05.01.2019
comment
@ user10865622: это означает, что ветка прыгает, а не проваливается. например jnz, если он запускается, когда ZF очищен. Безусловные переходы, такие как jmp и call, всегда выполняются, но условные переходы (jcc) могут не выполняться. Интересный факт: haswell и более поздние версии имеют модуль выполнения ветвления на порту 0, который может обрабатывать только мопы «предсказанное непринятое ветвление» и «макро-объединенное ветвление», а также обычный модуль выполнения ветвления на порту 6, который может обрабатывать любую ветвь. - person Peter Cordes; 05.01.2019