我正在寻找一种公式/方式来衡量一条指令的速度,或更具体地讲,是按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
我知道程序中指令的顺序在这里很重要,但是 我正在寻找一种通用的东西,不需要“精确到单个周期”
有人知道我该怎么做吗?
非常感谢
答案 0 :(得分:6)
没有可以应用的公式;您必须进行测量。
在同一uarch系列的不同版本上的同一指令可能具有不同的性能。例如mulps
:
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。因此,简单的寻址模式快速路径仅有助于纯指针跟踪。)