汇编 - 如何通过延迟和吞吐量对CPU指令进行评分

问题描述 投票:2回答:1

我正在寻找一种公式/方法来衡量一条指令的速度,或者更具体地说是通过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

我知道程序中的指令顺序在这里很重要,但我希望创建一些通用的东西,不需要“精确到单循环”

任何人都知道我该怎么做?

非常感谢

performance assembly x86 x86-64 micro-optimization
1个回答
6
投票

没有可以申请的公式;你必须衡量。

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

  • Sandybridge 1c / 5c吞吐量/延迟。
  • HSW 0.5 / 5.BDW 0.5 / 3(FMA单元中的乘法路径更快?FMA仍为5c)。
  • SKL 0.5 / 4(低延迟FMA也是如此)。 SKL也在FMA单元上运行addps,丢弃专用FP乘法单元,因此增加延迟更高,但吞吐量更高。

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


您可以通过在展开的循环中多次重复指令来进行测量。或者完全展开而没有循环,但是你打败了uop-cache并且可以获得前端瓶颈。 (例如,用于解码10字节的mov r64, imm64

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

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


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

 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次迭代中阻塞循环。使用perf计数器进行核心时钟周期(不是RDTSC周期),您可以轻松地将所有迭代的时间测量为10k中的1个部分,并且可能更精确地比这更精确。无论您使用何种时间,重复计数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链的无序执行窗口的复杂性。


要测量吞吐量,可以使用单独的寄存器,和/或偶尔包含xor-zeroing来破坏dep链并让乱序exec重叠。不要忘记也使用perf计数器来查看它可以运行的端口,这样你就可以知道它将与哪些其他指令竞争。 (例如,FMA(p01)和shuffles(p5)根本不竞争Haswell / Skylake的后端资源,仅用于前端吞吐量。)不要忘记测量前端uop计数:一些指令解码以乘以uop。

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

例如,如果FP乘法具有0.25c吞吐量(每个时钟4个),我们可以在Haswell(5c延迟)上同时保持20个飞行。这比我们注册的更多,所以我们可以使用全部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 exec重叠,而不仅仅是当sub写入所有标志时。 (见INC instruction vs ADD 1: Does it matter?

在Sandybridge系列上测量shl r32, cl的吞吐量和延迟存在类似的问题:标志依赖链通常与计算无关,但将shl背对背放置会通过FLAGS和寄存器创建依赖关系。 (或者对于吞吐量,甚至没有寄存器dep)。

我在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.BMI2 shlx 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 exec的内容足够小以重叠不同的部分,因此指令延迟和吞吐量都很重要。)


加载/存储的Latency=2数字似乎来自Agner Fog的指令表(https://agner.org/optimize/)。不幸的是,它们对mov rax, [rax]链并不准确。如果你把它放在一个循环中测量它,你会发现4c延迟。

Agner将加载/存储延迟分解为使总存储/重新加载延迟正确的东西,但由于某种原因,当缓存来自缓存而不是存储时,他不会使加载部分等于L1d加载使用延迟缓冲。 (但是请注意,如果加载提供ALU指令而不是另一个加载,则延迟为5c。因此简单的寻址模式快速路径仅对纯指针追逐有帮助。)

© www.soinside.com 2019 - 2024. All rights reserved.