我正在寻找一种公式/方法来衡量一条指令的速度,或者更具体地说是通过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
我知道程序中的指令顺序在这里很重要,但我希望创建一些通用的东西,不需要“精确到单循环”
任何人都知道我该怎么做?
非常感谢
没有可以申请的公式;你必须衡量。
同一uarch系列的不同版本上的相同指令可以具有不同的性能。例如mulps
:
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。因此简单的寻址模式快速路径仅对纯指针追逐有帮助。)