Короткая версия:
На Intel i7 есть какие-то узкие места в доступе к «оригинальным» регистрам (eax
, ebx
, ecx
, edx
), которых нет в «новых» регистрах (r8d
, r9d
и т. Д.)? Я синхронизирую некоторый код, и, если я попытаюсь запустить три инструкции добавления параллельно, я могу получить CPI 0,33, при условии, что только две из трех операций добавления ссылаются на «оригинальный» регистр (например, я использую eax
, ebx
и r9d
). Если я попытаюсь использовать три «оригинальных» регистра, индекс потребительских цен увеличится примерно до 0,4. Я наблюдал как на i7-3770, так и на i7-4790.
Подробности:
Я пытаюсь разработать новую (надеюсь, интересную) лабораторию для своего класса по компьютерной архитектуре. Цель состоит в том, чтобы они рассчитали некоторый ассемблерный код на процессоре Intel i7 и наблюдали такие вещи, как (а) пропускная способность процессора и (б) последствия зависимости данных.
Когда я пытаюсь написать какой-то ассемблерный код, который показывает средний CPI 0,33 (т. Е. Демонстрирует, что ЦП может поддерживать пропускную способность 3 инструкции за такт), я обнаружил, что это возможно только в том случае, если не более двух из три инструкции обращаются к «оригинальным» регистрам общего назначения.
Настройка эксперимента
Вот основной план эксперимента: используйте rdtsc
для определения временных сегментов нескольких тысяч инструкций, затем выведите «счетчик циклов» в зависимости от количества команд, рассчитанных для оценки пропускной способности. Например, выполнение этого кода внутри цикла
mov $0, %eax
cpuid
rdtsc
movl %eax, %r12d
addl $1, %eax
addl $1, %eax
# the line above is copied "n" times
# (I use a ruby script to generate this part of the assembly)
addl $1, %eax
rdtsc
subl %r12d, %eax
позволяет нам сообщить, сколько времени (в ссылочных циклах) требуется для выполнения последовательности n
addl
инструкций. (Приведенный выше фрагмент кода является частью более длинной программы, которая многократно повторяет измерение, отбрасывает первые несколько тысяч испытаний и сообщает о самом низком и / или наиболее распространенном результате.)
Результаты, которые имеют смысл
Когда я вычисляю последовательность операций добавления в один регистр, я получаю ожидаемый результат:
instructions elapsed reference ref cycles estimated actual actual cycles
between rdtsc cycles per instruction cycles per instruction
200 145 0.72 169 0.84
300 220 0.73 256 0.85
400 314 0.79 365 0.91
500 408 0.82 474 0.95
600 483 0.81 562 0.94
700 577 0.82 671 0.96
800 652 0.81 758 0.95
900 746 0.83 867 0.96
1000 840 0.84 977 0.98
1100 915 0.83 1064 0.97
1200 1009 0.84 1173 0.98
........................................................................
3500 3019 0.86 3510 1.00
3600 3094 0.86 3598 1.00
3700 3188 0.86 3707 1.00
3800 3282 0.86 3816 1.00
3900 3357 0.86 3903 1.00
4000 3451 0.86 4013 1.00
После преобразования эталонных циклов в (предполагаемые) фактические циклы процессор усредняет примерно одну инструкцию за цикл. Это имеет смысл, поскольку каждая синхронизированная инструкция зависит от предыдущей инструкции, тем самым предотвращая параллельное выполнение. Обратите внимание, что мы не выдаем команду сериализации до окончания rdtsc
. В результате последние несколько дюжин синхронизированных инструкций еще не завершены, когда мы «останавливаем» таймер. Следовательно, ИПЦ для первых нескольких строк этой таблицы является искусственно низким. Эффект этого «недоучета» ограничивается до нуля по мере увеличения количества команд по времени.
Если мы изменим временной код для чередования дополнений к eax
и ebx
, мы также получим ожидаемый результат: ИПЦ, который стремится к 0,5:
mov $0, %eax
cpuid
rdtsc
movl %eax, %r12d
addl $1, %eax
addl $1, %ebx
addl $1, %eax
addl $1, %ebx
# the pair of lines above are copied until there are `n` lines total being timed
addl $1, %eax
addl $1, %ebx
rdtsc
subl %r12d, %eax
instructions elapsed reference ref cycles estimated actual actual cycles
between rdtsc cycles per instruction cycles per instruction
1000 432 0.43 502 0.50
1200 510 0.42 593 0.49
1400 601 0.43 699 0.50
1600 695 0.43 808 0.51
1800 773 0.43 899 0.50
2000 864 0.43 1005 0.50
2200 955 0.43 1110 0.50
Вопрос: Почему имеет значение, какой регистр я использую при попытке запустить 3 инструкции параллельно?
Когда я пытаюсь запустить добавление к eax
, ebx
и ecx
параллельно, ИПЦ выше ожидаемого .33:
mov $0, %eax
cpuid
rdtsc
movl %eax, %r12d
addl $1, %eax
addl $1, %ebx
addl $1, %ecx
addl $1, %eax
addl $1, %ebx
addl $1, %ecx
# the group of lines above are copied until there are `n` lines total being timed
addl $1, %eax
addl $1, %ebx
addl $1, %ecx
rdtsc
subl %r12d, %eax
instructions elapsed reference ref cycles estimated actual actual cycles
between rdtsc cycles per instruction cycles per instruction
1200 408 0.34 474 0.40
1500 492 0.33 572 0.38
1800 595 0.33 692 0.38
2100 698 0.33 812 0.39
2400 782 0.33 909 0.38
2700 885 0.33 1029 0.38
3000 988 0.33 1149 0.38
3300 1091 0.33 1269 0.38
3600 1178 0.33 1370 0.38
Однако я получаю ожидаемый результат, если использую r9d
, r10d
и r11d
:
instructions elapsed reference ref cycles estimated actual actual cycles
between rdtsc cycles per instruction cycles per instruction
1200 350 0.29 407 0.34
1500 444 0.30 516 0.34
1800 519 0.29 603 0.34
2100 613 0.29 713 0.34
2400 707 0.29 822 0.34
2700 782 0.29 909 0.34
3000 876 0.29 1019 0.34
Фактически, я получаю ожидаемый результат, если максимум два из трех регистров поступают из набора eax
, ebx
, ecx
и edx
. Это почему? Любая идея, является ли узкое место в проблеме, декодировании, переименовании регистра или выходе на пенсию?
Я наблюдал такое поведение на i7-3770 и i7-4790. Для чего это стоит: у Ryzen 7 и i5-6500 всегда есть ИПЦ от 0,38 до 0,40, независимо от используемых регистров.
код
Для тех, кому интересно, вот шаблон для кода, который я использую:
.file "timestamp_shell.c"
.text
.section .rodata
.align 8
.LC0:
.string "%8d; Start %10u; Stop %10u; Difference %5d\n"
.text
.globl main
.type main, @function
main:
.LFB0:
.cfi_startproc
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
pushq %r13
pushq %r12
pushq %rbx
subq $8, %rsp
.cfi_offset 13, -24
.cfi_offset 12, -32
.cfi_offset 3, -40
movl $100, %r12d
movl $200, %r13d
movl $-1, %r8d
movl $0, %r8d
jmp .L2
.L3:
mov $0, %eax
cpuid
rdtsc
movl %eax, %r12d
movl $0, %eax
# I use a perl script to copy the lines marked with #@ until there
# is the desired number of instructions between the calls to rdstc
#@ addl $1, %eax
#@ addl $1, %r10d
#@ addl $1, %ecx
rdtsc
subl %r12d, %eax
movl %eax, %r8d
movl %r13d, %ecx
movl %r12d, %edx
movl %r8d, %esi
leaq .LC0(%rip), %rdi
movl $0, %eax
call printf@PLT
addl $1, %r8d
.L2:
cmpl $999999, %r8d
jle .L3
movl $199, %eax
addq $8, %rsp
popq %rbx
popq %r12
popq %r13
popq %rbp
.cfi_def_cfa 7, 8
ret
.cfi_endproc
.LFE0:
.size main, .-main
.ident "GCC: (GNU) 8.2.1 20181127"
.section .note.GNU-stack,"",@progbits