汇编-如何通过等待时间和吞吐量对CPU指令进行评分

时间:2018-09-10 15:01:06

标签: performance assembly x86 x86-64 micro-optimization

我正在寻找一种公式/方式来衡量一条指令的速度,或更具体地讲,是按CPU周期为每条指令提供“分数”。

以以下汇编程序为例,

nop                     
mov         eax,dword ptr [rbp+34h] 
inc         eax     
mov         dword ptr [rbp+34h],eax  

以及以下英特尔Skylake信息:

  

mov r,m:吞吐量= 0.5延迟= 2

     

mov m,r    :吞吐量= 1延迟= 2

     

nop:吞吐量= 0.25延迟=非

     

inc:吞吐量= 0.25延迟== 1

我知道程序中指令的顺序在这里很重要,但是 我正在寻找一种通用的东西,不需要“精确到单个周期”

有人知道我该怎么做吗?

非常感谢

1 个答案:

答案 0 :(得分:6)

没有可以应用的公式;您必须进行测量

在同一uarch系列的不同版本上的同一指令可能具有不同的性能。例如mulps

  • Sandybridge 1c / 5c吞吐量/延迟。
  • HSW 0.5 /5。BDW0.5 / 3(FMA单位中的乘积路径更快?FMA仍为5c)。
  • SKL 0.5 / 4(更低的延迟FMA)。 SKL也在FMA单元上运行addps,删除了专用FP乘法单元,因此增加了等待时间,但是吞吐量更高。

如果不进行测量或不了解某些微体系结构细节,就无法预测其中的任何一个。我们希望FP数学运算不会出现单周期延迟,因为它们比整数运算要复杂得多。 (因此,如果它们是单周期,则对于整数运算而言,时钟速度设置得太低。)


您可以通过展开循环中的多次指令来进行测量。或者完全展开而没有循环,但是您随后击败了uop缓存并可能遇到前端瓶颈。 (例如,用于解码10字节的mov r64, imm64

Agner Fog通过定时重复指令的大型非循环代码块来创建他的指令表(您似乎正在阅读)。 https://agner.org/optimize/。他的指令表的简介部分简要介绍了他的测量方法,而他的微体系结构指南则详细介绍了不同x86微体系结构在内部如何工作。

http://instlatx64.atw.hu/也具有实验测量结果。我认为他们使用类似的技术,即重复执行同一条指令的大块,可能足够小以适合uop缓存。但是他们不使用性能计数器来衡量每条指令所需的执行端口,因此它们的吞吐量数字无法帮助您确定哪些指令与其他指令竞争。


要自己测量延迟,可以将每条指令的输出作为下一条指令的输入。

 mov  ecx, 10000000
 inc_latency:
     inc eax
     inc eax
     inc eax
     inc eax
     inc eax
     inc eax

     sub ecx,1          ; avoid partial-flag false dep for P4
     jnz inc_latency    ; dec or sub/jnz macro-fuses into 1 uop on Intel SnB-family

这条包含7条inc指令的依赖链将在每7 * inc_latency个周期进行1次迭代时使循环成为瓶颈。使用针对核心时钟周期(而非RDTSC周期)的性能计数器,您可以轻松地将 all 迭代的时间测量为1k的1k,并且可能要更精确一些。重复计数10000000会隐藏您使用任何时间的启动/停止开销。

我通常在Linux静态可执行文件中放置一个这样的循环,该循环仅通过sys_exit(0)指令直接进行syscall系统调用,并用perf stat ./testloop将整个可执行文件计时到得到时间和周期数。 (有关示例,请参见Can x86's MOV really be "free"? Why can't I reproduce this at all?

另一个示例是Understanding the impact of lfence on a loop with two long dependency chains, for increasing lengths,使用lfence消耗两个dep链的无序执行窗口会增加复杂性。


要测量吞吐量,您可以使用单独的寄存器,和/或偶尔使用异或归零法来中断dep链并让乱序的exec重叠。性能计数器可以查看它可以在哪些端口上运行,因此您可以知道它将与哪些其他指令竞争。 (例如FMA(p01)和shuffle(p5)根本不争夺Haswell / Skylake上的后端资源,仅竞争前端吞吐量。)也不要忘记衡量前端uop计数:指令解码以加乘。

我们需要多少个不同的依赖链来避免瓶颈?好了,我们知道了延迟(首先测量它),我们知道了最大可能的吞吐量(执行端口数或前端吞吐量)。

例如,如果FP乘法具有0.25c的吞吐量(每个时钟4个),我们可以在Haswell上一次保持20个飞行(5c的延迟)。这比我们拥有的寄存器还要多,因此我们只需要使用全部16个寄存器,就会发现实际上吞吐量仅为0.5c。但是,如果发现16个寄存器是一个瓶颈,我们可以偶尔添加xorps xmm0,xmm0并让乱序执行重叠某些块。

通常情况下,越多越好;仅仅不足以隐藏延迟的时间可能会因调度不完善而减慢速度。如果我们想度量inc,可以这样做:

 mov  ecx, 10000000
 inc_latency:
   %rep 10          ;; source-level repeat of a block, no runtime branching
     inc eax
     inc ebx
     ; not ecx, we're using it as a loop counter
     inc edx
     inc esi
     inc edi
     inc ebp
     inc r8d
     inc r9d
     inc r10d
     inc r11d
     inc r12d
     inc r13d
     inc r14d
     inc r15d
   %endrep

     sub ecx,1          ; break partial-flag false dep for P4
     jnz inc_latency    ; dec/jnz macro-fuses into 1 uop on Intel SnB-family

如果我们担心部分标志错误的依赖性或标志合并的影响,我们可以尝试在xor eax,eax处混合以使OoO执行程序重叠,而不仅仅是sub写所有标志时。 (请参见INC instruction vs ADD 1: Does it matter?

在Sandybridge系列上测量shl r32, cl的吞吐量和等待时间也存在类似的问题:标记依赖关系链通常与计算无关,但是将shl背对背放置会产生一个通过FLAGS和寄存器进行依赖。 (或者对于吞吐量,甚至没有寄存器深度)。

我在Agner Fog的博客上发布了此内容:https://www.agner.org/optimize/blog/read.php?i=415#860。我将shl edx,cl与四个add edx,1指令混合在一起,以查看增加了另一条指令的增量减慢情况,其中FLAGS依赖性不是问题。在SKL上,它平均只会平均减少额外的1.23个周期,因此该shl的真实延迟成本仅为〜1.23个周期,而不是2个周期。(这不是整数,或者仅仅是1个,因为资源冲突我猜运行shl的标志合并uops。BMI2shlx edx, edx, ecx恰好是1c,因为它只是一个uop。


相关:有关整个代码块(包含不同指令)的静态性能分析,请参见What considerations go into predicting latency for operations on modern superscalar processors and how can I calculate them by hand? 。 (它在整个计算的端到端延迟中使用了“延迟”一词,但实际上是在询问足以使OoO执行程序重叠不同部分的小事情,因此指令延迟和吞吐量都很重要。)


用于加载/存储的Latency=2数字似乎来自Agner Fog的指令表(https://agner.org/optimize/)。不幸的是,对于一连串mov rax, [rax],它们并不准确。您会发现这是4分  如果将其放在一个循环中进行测量,则会导致延迟。

Agner将加载/存储延迟划分为可以使总存储/重新加载延迟正确显示的内容,但是由于某种原因,当加载来自缓存时,他没有使加载部分等于L1d加载使用延迟存储缓冲区的大小。 (但也请注意,如果负载提供的是ALU指令而不是其他负载,则延迟为5c。因此,简单的寻址模式快速路径仅有助于纯指针跟踪。)