Является ли последовательность команд вызова / возврата x86 образует зависимую цепочку?
Рассмотрим следующую сборку x86-64:
inner:
...
ret
outer:
.top:
call inner
dec rdi
jnz .top
ret
Функция outer
просто неоднократно делает call
к функции inner
(чье тело не показано - оно может быть пустым).
Делает серию call
инструкции в outer
и соответствующий ret
инструкции внутри inner
сформировать зависимую цепочку на практике (для оценки эффективности)?
Существует несколько способов, которыми эта цепочка может быть сформирована. Например, ret
зависит от времени ожидания предыдущего call
инструкция, а затем делает последующее call
инструкция зависит от ret
, образуя call -> ret -> call
цепь? Или возможно ret
является независимым, но call
нет, образуя call -> call
цепь? Если есть цепочка, это через память, регистр, механизм стека, предиктор адреса возврата1 или что?
Мотивация: Этот вопрос возник из серии комментариев к другому вопросу, в основном этот комментарий и более ранние.
1 Терминология здесь может быть несколько неясной: механизм стека обычно понимается как обрабатывающий rsp
- изменение инструкций в единый доступ с соответствующим смещением, так что push rax; push rbx
может быть преобразован в нечто подобное mov [t0], rax; mov [t0 - 8], rbx
где t0
некоторый временный регистр, который захватил значение rsp
в какой-то момент. Понятно также обрабатывать аналогичные преобразования для call
а также ret
инструкции, которые оба модифицируют стек аналогично push
а также pop
а также включая прямой, косвенный (соответственно) прыжок. Процессор также включает в себя механизм для прогнозирования обратного непрямого скачка, который частично определяется как "механизм стека", но здесь я разделяю его на "предиктор обратного адреса".
1 ответ
Нет, предсказание ветвления + умозрительное выполнение нарушают зависимость сохранения / перезагрузки.
RIP (спекулятивно) известен внешнему интерфейсу от предиктора обратного адреса. Следующий call
Таким образом, инструкция может выдвинуть обратный адрес, не дожидаясь ret
выполнить (и фактически загрузить и проверить правильность предсказанного адреса возврата по отношению к данным из стека).
Спекулятивные хранилища могут входить в буфер хранилища и перенаправляться в хранилище.
Конечно, есть цепочка зависимостей, она не переносится циклами. Внеочередное выполнение скрывает это, сохраняя много итераций в полете.
Доказательство: call
Магазин ломает то, что в противном случае было бы цепочкой зависимостей, переносимых циклом.
align 64
global _start
_start:
mov ebp, 250000000 ; I had been unrolling by 4, should have changed this to 5000... before measuring, but forgot.
align 32
.mainloop:
call delay_retaddr
call delay_retaddr
dec ebp
jg .mainloop
xor edi,edi
mov eax,231 ; __NR_exit_group from /usr/include/asm/unistd_64.h
syscall ; sys_exit_group(0)
;; Placing this function *before* _start, or increasing the alignment,
;; makes it somewhat slower!
align 32
delay_retaddr:
add qword [rsp], 0
add qword [rsp], 0 ; create latency for the ret addr
ret
Собрать и связать с yasm -felf64 -Worphan-labels -gdwarf2 foo.asm && ld -o foo foo.o
, производящий статический бинарный файл ELF.
Профилированный (на i7-6700k) с ocperf.py, я получаю 0,99 инструкций за такт ядра:
$ taskset -c 3 ocperf.py stat -etask-clock,context-switches,cpu-migrations,page-faults,cycles,branches,instructions,uops_issued.any,uops_executed.thread,dsb2mite_switches.penalty_cycles -r2 ./foo
Performance counter stats for './foo' (2 runs):
645.770390 task-clock (msec) # 1.000 CPUs utilized ( +- 0.05% )
1 context-switches # 0.002 K/sec ( +-100.00% )
0 cpu-migrations # 0.000 K/sec
2 page-faults # 0.004 K/sec ( +- 20.00% )
2,517,412,984 cycles # 3.898 GHz ( +- 0.09% )
1,250,159,413 branches # 1935.919 M/sec ( +- 0.00% )
2,500,838,090 instructions # 0.99 insn per cycle ( +- 0.00% )
4,010,093,750 uops_issued_any # 6209.783 M/sec ( +- 0.03% )
7,010,150,784 uops_executed_thread # 10855.485 M/sec ( +- 0.02% )
62,838 dsb2mite_switches_penalty_cycles # 0.097 M/sec ( +- 30.92% )
0.645899414 seconds time elapsed ( +- 0.05% )
С вызываемой функцией перед _start
и значения выравнивания 128
IPC может снизиться с 0,99 до 0,84, что очень странно. Количество переключателей dsb2mite по-прежнему низкое, поэтому в основном он все еще работает из кэша UOP, а не из устаревших декодеров. (Этот процессор Skylake имеет обновление микрокода, которое отключает буфер цикла, в случае, если это будет актуально при всех этих переходах.)
Чтобы поддерживать хорошую пропускную способность, ЦП должен поддерживать много итераций внутреннего цикла в полете, потому что мы значительно удлинили независимые цепочки развертывания, которые должны перекрываться.
Изменение add [rsp], 0
инструкции к [rsp+16]
создает переносимую циклом цепочку зависимостей в другом месте, которая не сохраняется call
, Таким образом, циклические узкие места в этой задержке пересылки магазина работают примерно на половине скорости.
# With add qword [rsp+16], 0
Performance counter stats for './foo' (2 runs):
1212.339007 task-clock (msec) # 1.000 CPUs utilized ( +- 0.04% )
2 context-switches # 0.002 K/sec ( +- 60.00% )
0 cpu-migrations # 0.000 K/sec
2 page-faults # 0.002 K/sec
4,727,361,809 cycles # 3.899 GHz ( +- 0.02% )
1,250,292,058 branches # 1031.306 M/sec ( +- 0.00% )
2,501,537,152 instructions # 0.53 insn per cycle ( +- 0.00% )
4,026,138,227 uops_issued_any # 3320.967 M/sec ( +- 0.02% )
7,026,457,222 uops_executed_thread # 5795.786 M/sec ( +- 0.01% )
230,287 dsb2mite_switches_penalty_cycles # 0.190 M/sec ( +- 68.23% )
1.212612110 seconds time elapsed ( +- 0.04% )
Обратите внимание, что я все еще использую RSP-относительный адрес, так что все еще есть синхронизация стека. Я мог бы сохранить оба случая одинаковыми и избежать их в обоих, используя адрес относительно другого регистра (например, rbp
) обратиться к месту, где call
/ ret
сохранить / перезагрузить обратный адрес.
Я не думаю, что переменная задержка переадресации магазина (хуже в простых случаях прямой перезагрузки сразу) достаточна, чтобы объяснить разницу. Добавление избыточного назначения ускоряет код при компиляции без оптимизации. Это в 2 раза ускоряет нарушение зависимости. (0,99 IPC против 0,53 IPC, с одинаковыми инструкциями, только в другом режиме адресации.)
Инструкции длиннее на 1 байт с disp8
в режиме адресации, и в более быстрой версии была странность внешнего интерфейса с выравниванием, но перемещение объектов, похоже, ничего не меняет с [rsp+16]
версия.
Использование версии, которая создает стойло для пересылки магазина (с add dword [rsp], 0
) делает цепочку депо слишком длинной, чтобы OoO exec легко скрывался. Я не играл с этим огромное количество.