Ответ 1
Обновление. Задержка хранения/перезагрузки Skylake достигает 3 с, но только если время правильное. Последовательные нагрузки, задействованные в цепочке зависимостей перенаправления хранения, которые естественным образом распределены на 3 или более циклов, будут испытывать более высокую задержку (например, с 4 imul eax,eax
в цикле, mov [rdi], eax
/mov eax, [rdi]
учитывает только цикл От 12 до 15 циклов на итерацию.), Но когда нагрузки позволяют выполнять более плотно, чем это, страдает некоторый тип разногласий, и вы получаете около 4,5 циклов на итерацию. Нецелая средняя пропускная способность также является большой подсказкой, что есть что-то необычное.
Я видел тот же эффект для векторов 32B (наилучший вариант 6.0c, back-to-back 6.2 до 6.9c), но векторы 128b всегда были вокруг 5.0c. См. подробности на форуме Agner Fog.
При правильном (неправильном) выравнивании экстренный call
в цикле может фактически помочь Skylake наблюдать меньшую задержку пересылки в магазине от push to pop. Я смог воспроизвести это с помощью perf counters (Linux perf stat -r4
), используя YASM. (Я слышал, что менее удобно использовать перфорированные счетчики в Windows, и в любом случае у меня нет машины Windows dev. К счастью, ОС не имеет отношения к ответу: кто-то должен иметь возможность воспроизводить результаты моего перм-счетчика на Windows с VTune или что-то в этом роде.)
Я видел более быстрые времена при смещении = 0..10, 37, 63-74, 101 и 127 после align 128
в месте, указанном в вопросе. Линии кэша L1I равны 64B, а uop-cache заботится о границах 32B. Он выглядит выравниванием относительно границы 64B - это все, что имеет значение.
Цикл без вызова - это постоянный 5 циклов, но цикл call
может опускаться до 4c на итерацию из его обычных почти точно-5 циклов. Я видел более медленную, чем обычно, производительность при смещении = 38 (5,68 + - 8,3% циклов на итерацию). Есть небольшие сбои в других точках, например 5.17c + - 3.3%, согласно perf stat -r4
(что делает 4 прогона и усреднение).
Кажется, что взаимодействие между front-end не ставит в очередь так много uops впереди, что приводит к тому, что задний конец имеет более низкую задержку для хранения-пересылки с push на pop.
IDK, если повторное использование одного и того же адреса повторно для перенаправления хранилища делает его более медленным (с несколькими хранимыми адресами uops, уже выполненными перед соответствующими данными хранения данных), или что.
Тестовый код: bash
shell loop для создания и профилирования asm с каждым другим смещением:
(set -x; for off in {0..127};do
asm-link -m32 -d call-tight-loop.asm -DFUNC=normal_call -DOFFSET=$off &&
ocperf.py stat -etask-clock,context-switches,cpu-migrations,page-faults:u,cycles,instructions,uops_issued.any,uops_executed.thread,idq.mite_uops,dsb2mite_switches.penalty_cycles -r4 ./call-tight-loop;
done ) |& tee -a call-tight-loop.call.offset-log
(set -x)
в подоболочке - это удобный способ записи команд вместе с их выходом при перенаправлении в файл журнала.
asm-link
- это script, который запускает yasm -felf32 -Worphan-labels -gdwarf2 call-tight-loop.asm "[email protected]" && ld -melf_i386 -o call-tight-loop call-tight-loop.o
, затем запускает objdumps -drwC -Mintel
в результате.
Программа тестирования NASM/YASM Linux (собирается в полный статический двоичный файл, который запускает цикл, а затем завершает работу, поэтому вы можете профилировать всю программу.) Прямой порт источника OP FASM без оптимизации для asm.
CPU p6 ; YASM directive. For NASM, %use smartalign.
section .text
iter equ 100000000
%ifndef OFFSET
%define OFFSET 0
%endif
align 128
;;offset equ 23 ; this is the number I am changing
times OFFSET nop
times 16 nop
no_call:
mov ecx, iter
.loop:
push ecx
pop ecx
dec ecx
cmp ecx, 0
jne .loop
ret
times 55 nop
normal_function:
ret
times 58 nop
normal_call:
mov ecx, iter
.loop:
push ecx
call normal_function
pop ecx
dec ecx
cmp ecx, 0
jne .loop
ret
%ifndef FUNC
%define FUNC no_call
%endif
align 64
global _start
_start:
call FUNC
mov eax,1 ; __NR_exit from /usr/include/asm/unistd_32.h
xor ebx,ebx
int 0x80 ; sys_exit(0), 32-bit ABI
Пример вывода из быстрого запуска call
:
+ asm-link -m32 -d call-tight-loop.asm -DFUNC=normal_call -DOFFSET=3
...
080480d8 <normal_function>:
80480d8: c3 ret
...
08048113 <normal_call>:
8048113: b9 00 e1 f5 05 mov ecx,0x5f5e100
08048118 <normal_call.loop>:
8048118: 51 push ecx
8048119: e8 ba ff ff ff call 80480d8 <normal_function>
804811e: 59 pop ecx
804811f: 49 dec ecx
8048120: 83 f9 00 cmp ecx,0x0
8048123: 75 f3 jne 8048118 <normal_call.loop>
8048125: c3 ret
...
Performance counter stats for './call-tight-loop' (4 runs):
100.646932 task-clock (msec) # 0.998 CPUs utilized ( +- 0.97% )
0 context-switches # 0.002 K/sec ( +-100.00% )
0 cpu-migrations # 0.000 K/sec
1 page-faults:u # 0.010 K/sec
414,143,323 cycles # 4.115 GHz ( +- 0.56% )
700,193,469 instructions # 1.69 insn per cycle ( +- 0.00% )
700,293,232 uops_issued_any # 6957.919 M/sec ( +- 0.00% )
1,000,299,201 uops_executed_thread # 9938.695 M/sec ( +- 0.00% )
83,212,779 idq_mite_uops # 826.779 M/sec ( +- 17.02% )
5,792 dsb2mite_switches_penalty_cycles # 0.058 M/sec ( +- 33.07% )
0.100805233 seconds time elapsed ( +- 0.96% )
Старый ответ, прежде чем замечать задержку хранения в хранилище переменных
Вы нажимаете/выписываете свой счетчик циклов, поэтому все, кроме инструкций call
и ret
(и cmp
/jcc
), являются частью цепочки зависимостей, зависящей от цикла, с использованием счетчика циклов.
Вы ожидаете, что pop
придется ждать обновления указателя стека на call
/ret
, но механизм стека обрабатывает эти обновления с нулевой задержкой. (Intel с Pentium-M, AMD с K10, согласно Agtern Fog microarch pdf, поэтому я предполагаю, что ваш процессор имеет один, хотя вы ничего не сказали о том, на какой микроархитектуре процессора вы проводили тесты.)
Дополнительный call
/ret
по-прежнему необходимо выполнить, но выполнение вне очереди может поддерживать выполнение команд критического пути с максимальной пропускной способностью. Так как это включает в себя латентность пересылки данных store- > load из цикла push/pop + 1 для dec
, это не высокая пропускная способность на любом процессоре, и это удивительно, что интерфейсный сервер может быть узким местом с любым выравниванием.
push
→ pop
латентность - это 5 циклов на Skylake, согласно Agner Fog, и так далее, что ваш цикл может работать только в лучшем случае за одну итерацию за 6 циклов.
Для выполнения команд call
и ret
достаточно времени для выполнения вне очереди. Agner перечисляет максимальную пропускную способность для call
одного на 3 цикла, а ret
- по одному на 1 цикл. Или на AMD Bulldozer, 2 и 2. В его таблицах ничего не говорится о пропускной способности пары call
/ret
, поэтому IDK могут ли они перекрываться или нет. На AMD Bulldozer задержка хранения/перезагрузки с mov
составляет 8 циклов. Я предполагаю, что примерно то же самое с push/pop.
Похоже, что различные выравнивания для вершины цикла (т.е. no_call.loop_start:
) вызывают узкие места переднего плана. Версия call
имеет три ветки на итерацию: вызов, ret и ветвь цикла. Обратите внимание, что целью ветвления ret
является инструкция сразу после call
. Каждый из них потенциально нарушает интерфейс. Поскольку вы наблюдаете фактическое замедление на практике, мы должны видеть более 1 задержки цикла для каждой ветки. Или для версии no_call, одиночный пузырь выборки/декодирования хуже, чем около 6 циклов, что приводит к фактическому запущенному циклу при выпуске uops в нестандартную часть ядра. Это странно.
Слишком сложно догадаться, какие фактические микроархитектурные детали для каждого возможного uarch, поэтому дайте нам знать, на каком CPU вы тестировали.
Я упоминаю, что push
/pop
внутри цикла на Skylake останавливает его от выдачи из Loop Stream Detector и должен повторно извлекаться из кэша uop каждый раз. Руководство по оптимизации Intel говорит, что для Sandybridge несогласованный push/pop внутри цикла останавливает его от использования LSD. Это означает, что он может использовать LSD для петель со сбалансированным push/pop. В моем тестировании это не относится к Skylake (с помощью счетчика производительности lsd.uops
), но я не видел упоминания о том, было ли это изменение, или действительно ли это был SnB.
Кроме того, безусловные ветки всегда заканчивают линию uop-cache. Возможно, что с normal_function:
в том же естественно выровненном блоке 32B машинного кода как call
и jne
, возможно, блок кода не подходит в кэше uop. (Только 3 строки кэш-кеша могут кэшировать декодированные uops для одного 32-битового кода x86). Но это не объясняет возможности проблем для цикла no_call, поэтому вы, вероятно, не работаете в микроархитектуре семейства Intel SnB.
(обновление, да, цикл иногда выполняется в основном из унаследованного декодирования (idq.mite_uops
), но обычно не исключительно. dsb2mite_switches.penalty_cycles
обычно ~ 8k и, вероятно, происходит только при прерываниях таймера. Прогоны, где call
цикл работает быстрее, кажется, коррелирует с более низким idq.mite_uops
, но он все еще 34M + - 63% для случая offset = 37, где итерации 100M занимали 401M циклов.)
Это действительно один из тех случаев "не делай этого": встроенные крошечные функции вместо вызова изнутри очень плотных циклов.
Вы можете увидеть разные результаты, если вы push
/pop
регистр, отличный от вашего счетчика циклов. Это отделит push/pop от счетчика циклов, поэтому будет две отдельные цепи зависимостей. Он должен ускорить как версию вызова, так и no_call, но, возможно, неравномерно. Это может просто сделать узкое место в интерфейсе более очевидным.
Вы должны увидеть огромное ускорение, если вы push edx
, но pop eax
, поэтому инструкции push/pop не формируют цепочку зависимостей, связанных с циклом. Тогда дополнительный call
/ret
определенно будет узким местом.
Боковое примечание: dec ecx
уже устанавливает ZF так, как вы хотите, поэтому вы могли бы использовать только dec ecx / jnz
. Кроме того, cmp ecx,0
менее эффективен, чем test ecx,ecx
(больший размер кода и не может замаскироваться на столько CPU). Во всяком случае, совершенно не имеет отношения к вопросу об относительной производительности ваших двух циклов. (Недостаток директивы ALIGN
между функциями означает, что изменение первого изменило бы выравнивание ветки цикла во втором, но вы уже исследовали разные выравнивания.)