首页 > 解决方案 > 汇编 - 如何通过延迟和吞吐量对 CPU 指令进行评分

问题描述

我正在寻找一种公式/方法来衡量指令的速度,或者更具体地以 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 延迟=非

公司:吞吐量=0.25 延迟=1

我知道程序中指令的顺序在这里很重要,但我希望创建一些不需要“精确到单个周期”的通用内容

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

标签: performanceassemblyx86cpu-architecturemicro-optimization

解决方案


没有可以应用的公式;你必须测量

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

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

如果不进行测量或了解一些微架构细节,您就无法预测其中的任何内容。我们预计 FP 数学运算不会是单周期延迟,因为它们比整数运算复杂得多。(因此,如果它们是单周期,那么对于整数运算而言,时钟速度设置得太低。)


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

https://uops.info/已经对每条(非特权)指令的每一种形式进行了自动化测试,您甚至可以单击任何表格条目以查看它们使用了哪些测试循环。例如,从每个输入操作数到每个输出的Skylakexchg r32, eax延迟测试 ( https://uops.info/html-lat/SKL/XCHG_R32_EAX-Measurements.html )。(来自 EAX -> R8D 的 2 个周期延迟,但来自 R8D -> EAX 的 1 个周期延迟。)所以我们可以猜测这3 个微指令包括将 EAX 复制到内部临时,但直接从另一个操作数移动到 EAX。

https://uops.info/是目前最好的测试数据来源;当它和 Agner 的表格不一致时,我自己的测量和/或其他来源始终确认 uops.info 的测试是准确的。他们不会尝试为往返的 2 个半程(如 movd xmm0,eax 和返回)弥补延迟数,它们会向您展示可能的延迟范围,假设链的其余部分是最小的合理性。

Agner Fog 通过定时重复指令的大型非循环代码块来创建他的指令表(您似乎正在阅读)。 https://agner.org/optimize/。他的指令表的介绍部分简要解释了他如何测量,他的微架构指南解释了不同 x86 微架构内部如何工作的更多细节。不幸的是,他手工编辑的表格中偶尔会出现拼写错误或复制/粘贴错误。

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

后两个来源比 uops.info 存在的时间更长,并且涵盖了一些较旧的 CPU,尤其是较旧的 AMD。


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

 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

inc这个 7条指令的依赖链将在每个7 * inc_latency循环 1 次迭代时成为循环的瓶颈。将性能计数器用于核心时钟周期(不是 RDTSC 周期),您可以轻松地测量所有迭代到 10k 中的一部分的时间,而且可能比这更精确。10000000 的重复计数隐藏了您使用的任何时间的开始/停止开销。

我通常在 Linux 静态可执行文件中放置一个这样的循环,它只是sys_exit(0)直接(使用syscall)指令进行系统调用,并为整个可执行文件perf stat ./testloop计时以获取时间和循环计数。(请参阅x86 的 MOV 真的可以“免费”吗?为什么我根本不能重现这个?例如)。

另一个例子是了解 lfence 对具有两个长依赖链的循环的影响,以增加长度,并增加了使用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 exec 重叠更多,而不仅仅是在sub写入所有标志时。(参见INC 指令与 ADD 1:有关系吗?

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

我在 Agner Fog 的博客上发布了这个信息:https ://www.agner.org/optimize/blog/read.php?i=415#860 。我混合shl edx,cl了 4add edx,1条指令,以查看增加一条指令的增量减速,其中 FLAGS 依赖性不是问题。在 SKL 上,它平均只减慢了 1.23 个周期,因此真正的延迟成本shl仅为 1.23 个周期,而不是 2 个。(它不是整数或只是 1,因为运行标志合并的资源冲突的微指令shl,我猜。BMI2shlx edx, edx, ecx正好是 1c,因为它只是一个微指令。)


相关:对于整个代码块(包含不同指令)的静态性能分析,请参阅预测现代超标量处理器上的操作延迟的考虑因素以及如何手动计算它们?. (它使用“延迟”这个词来表示整个计算的端到端延迟,但实际上询问的事情足够小,以至于 OoO exec 可以重叠不同的部分,因此指令延迟和吞吐量都很重要。)


加载/存储的Latency=2数字似乎来自 Agner Fog 的指令表 ( https://agner.org/optimize/ )。不幸的是,它们对于mov rax, [rax]. 如果您通过将其放入循环中来测量它,您会发现这是 4c 延迟。

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


推荐阅读