x86-64 Относительная производительность jmp
В настоящее время я делаю назначение, которое измеряет производительность различных команд x86-64 (синтаксис at&t).
Команда, в которой я несколько запутался, это команда "безусловный jmp". Вот как я это реализовал:
.global uncond
uncond:
.rept 10000
jmp . + 2
.endr
mov $10000, %rax
ret
Это довольно просто. Код создает функцию с именем "uncond", которая использует директиву.rept для вызова команды jmp 10000 раз, а затем устанавливает возвращаемое значение равным количеству вызовов команды jmp.
"" в синтаксисе at&t означает текущий адрес, который я увеличил на 2 байта, чтобы учесть саму инструкцию jmp (поэтому jmp . + 2 должен просто перейти к следующей инструкции).
Код, который я не показал, вычисляет количество циклов, необходимых для обработки 10000 команд.
Мои результаты говорят, что jmp довольно медленный (для обработки одной команды jmp требуется 10 циклов), но из того, что я понимаю в конвейерной обработке, безусловные переходы должны быть очень быстрыми (без ошибок предсказания ветвлений).
Я что-то пропустил? Мой код неверен?
1 ответ
Процессор не оптимизирован для бездействия jmp
инструкции, поэтому он не обрабатывает особый случай продолжения декодирования и передачи команд jmp, которые просто переходят к следующему insn.
Однако процессоры оптимизированы для циклов. jmp .
будет работать по одному insn за такт на многих процессорах или один на 2 такта на некоторых процессорах.
Прыжок создает пузырь при получении инструкций. Один хорошо спрогнозированный прыжок - это нормально, но выполнить только прыжки проблематично. Я воспроизвел ваши результаты на Core2 E6600 (микроархив Merom/Conroe):
# jmp-test.S
.globl _start
_start:
mov $100000, %ecx
jmp_test:
.rept 10000
jmp . + 2
.endr
dec %ecx
jg jmp_test
mov $231, %eax
xor %ebx,%ebx
syscall # exit_group(0)
построить и запустить с:
gcc -static -nostartfiles jmp-test.S
perf stat -e task-clock,cycles,instructions,branches,branch-misses ./a.out
Performance counter stats for './a.out':
3318.616490 task-clock (msec) # 0.997 CPUs utilized
7,940,389,811 cycles # 2.393 GHz (49.94%)
1,012,387,163 instructions # 0.13 insns per cycle (74.95%)
1,001,156,075 branches # 301.679 M/sec (75.06%)
151,609 branch-misses # 0.02% of all branches (75.08%)
3.329916991 seconds time elapsed
Из другого прогона:
7,886,461,952 L1-icache-loads # 2377.687 M/sec (74.95%)
7,715,854 L1-icache-load-misses # 2.326 M/sec (50.08%)
1,012,038,376 iTLB-loads # 305.119 M/sec (75.06%)
240 iTLB-load-misses # 0.00% of all iTLB cache hits (75.02%)
(Числа в (%) в конце каждой строки показывают, сколько общего времени выполнения, для которого был активен счетчик: perf
должен мультиплексироваться для вас, когда вы просите его считать больше вещей, чем HW может сосчитать одновременно).
Так что на самом деле это не промахи I-кэша, это просто узкие места извлечения / декодирования инструкций, вызванные постоянными скачками.
Моя машина SnB сломана, поэтому я не могу тестировать числа на ней, но 8 циклов на джамп устойчивую пропускную способность очень близки к вашим результатам (которые, вероятно, были из другой микроархитектуры).
Для получения дополнительной информации см. http://agner.org/optimize/ и другие ссылки из вики-тега x86.