在i7上访问“原始”寄存器时是否存在瓶颈?

Zac*_*ack 6 optimization performance x86 intel cpu-architecture

简洁版本:

在Intel i7处理器,是有某种瓶颈访问的“原始”寄存器(中eaxebxecxedx)是不存在的“新”寄存器(r8dr9d,等)?我正在计时一些代码,如果尝试并行运行三个添加指令,则只要三个添加指令中只有两个引用“原始”寄存器(例如,我使用eaxebxr9d)。如果我尝试使用三个“原始”寄存器,则CPI最高约为0.4。我在i7-3770和i7-4790上都观察到了。

细节:

我试图为我的计算机体系结构课程开发一个新的(希望很有趣)实验室。他们的目标是让他们在Intel i7处理器上计时一些汇编代码,并观察(a)处理器的吞吐量和(b)数据依赖性的后果。

当我尝试编写一些显示平均CPI为0.33的汇编代码(即,表明CPU可以保持每个周期3条指令的吞吐量)时,我发现只有在最多访问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
Run Code Online (Sandbox Code Playgroud)

允许我们报告执行一系列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
Run Code Online (Sandbox Code Playgroud)

将参考周期转换为(估计的)实际周期后,处理器平均每个周期大约一条指令。这是有道理的,因为每个定时指令都依赖于前一条指令,从而阻止了并行执行。注意,我们在结束之前不会发出序列化指令rdtsc。结果,当我们“停止”计时器时,最后几十个计时指令尚未完成。因此,该表的前几行的CPI人为地降低了。随着定时指令数量的增加,该“计数不足”的影响限制为零。

如果我们修改定时代码以在eax和的添加之间交替ebx,我们还将获得预期的结果:CPI趋于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
Run Code Online (Sandbox Code Playgroud)

问:尝试并行运行3条指令时,我使用哪个寄存器有什么关系?

当我尝试运行添加到eaxebxecx并行运行时,CPI高于预期的.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
Run Code Online (Sandbox Code Playgroud)

但是,我得到预期的结果,如果我使用r9dr10d以及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
Run Code Online (Sandbox Code Playgroud)

事实上,我得到预期的结果,只要在最多两三个寄存器的来自集eaxebxecx,和edx。这是为什么?知道瓶颈是问题,解码,寄存器重命名还是退出?

我在i7-3770和i7-4790上都观察到了这种行为。物有所值:Ryzen 7和i5-6500始终具有0.38至.40的CPI,而与使用的寄存器无关。


代码

对于那些好奇的人,这是我使用的代码的模板:

.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
Run Code Online (Sandbox Code Playgroud)