汇编 - 如何根据延迟和吞吐量对 CPU 指令进行评分
Assembly - How to score a CPU instruction by latency and throughput
我正在寻找一种公式/方法来衡量一条指令的速度,或者更具体地说,通过 CPU 个周期给每条指令一个“分数”。
下面以汇编程序为例,
nop
mov eax,dword ptr [rbp+34h]
inc eax
mov dword ptr [rbp+34h],eax
以及以下英特尔 Skylake 信息:
mov r,m : Throughput=0.5 Latency=2
mov m,r
: Throughput=1 Latency=2
nop : Throughput=0.25 Latency=non
inc : Throughput=0.25 Latency=1
我知道程序中指令的顺序在这里很重要但是
我希望创建一些不需要“精确到单个周期”的通用内容
有人知道我该怎么做吗?
没有可以应用的公式;你必须测量.
同一指令在同一 uarch 系列的不同版本上可能具有不同的性能。例如mulps
:
- 桑迪布里奇 1c / 5c throughput/latency.
- HSW 0.5 / 5. BDW 0.5 / 3(FMA 单元中的乘法路径更快?FMA 仍然是 5c)。
- SKL 0.5 / 4(低延迟 FMA)。 FMA 单元上的 SKL 运行s
addps
也放弃了专用的 FP 乘法单元,因此添加延迟更高,但吞吐量更高。
如果不进行测量或不了解一些微体系结构细节,您就无法预测其中的任何一个。我们预计 FP 数学运算不会是单周期延迟,因为它们比整数运算复杂得多。 (因此,如果它们是单周期,则时钟速度对于整数操作设置得太低。)
您可以通过在展开的循环中多次重复指令来进行测量。或者 完全 展开而没有循环,但是你打败了 uop-cache 并且可能会遇到前端瓶颈。 (例如解码 10 字节 mov r64, imm64
)
https://uops.info/ has already automated this testing for every form of every (unprivileged) instruction, and you can even click on any table entry to see what test loops they used. e.g. Skylake xchg r32, eax
latency testing (https://uops.info/html-lat/SKL/XCHG_R32_EAX-Measurements.html) from each input operand to each output. (2 cycle latency from EAX -> R8D, but 1 cycle latency from R8D -> EAX.) So we can guess that ,而是直接从另一个操作数移动到EAX。
https://uops.info/是目前最好的测试数据来源;当它和 Agner 的表格不一致时,我自己的测量 and/or 其他来源总是证实 uops.info 的测试是准确的。而且他们不会尝试像 movd xmm0,eax 和 back 那样为往返的两半组成延迟数,他们会向您展示可能的延迟范围,假设链的其余部分是最合理的。
Agner Fog 通过计时重复指令的大型非循环代码块来创建他的指令表(您似乎正在阅读)。 https://agner.org/optimize/。他的指令表的介绍部分简要说明了他如何测量,他的微架构指南详细说明了不同 x86 微架构的内部工作方式。不幸的是,在他手工编辑的表格中偶尔会出现拼写错误或 copy/paste 错误。
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
这个 7 inc
指令的依赖链将在每 7 * inc_latency
周期 1 次迭代时成为循环瓶颈。将 perf 计数器用于核心时钟周期(而不是 RDTSC 周期),您可以轻松测量 all 迭代的时间到 10k 中的 1 部分,并且可能比这更精确。 10000000 的重复计数隐藏了 start/stop 您使用的任何计时的开销。
我通常在 Linux 静态可执行文件中放置一个这样的循环,它只是直接进行 sys_exit(0)
系统调用(使用 syscall
)指令,并为整个可执行文件计时perf stat ./testloop
获取时间和周期计数。 (有关示例,请参见 )。
另一个例子是,增加了使用lfence
来排除两个dep链的乱序执行window的复杂性。
为了测量吞吐量,您使用单独的寄存器,and/or偶尔包含一个异或归零来打破 dep 链并让无序的 exec 重叠。不要忘记也使用性能计数器来查看它可以 运行 在哪些端口上,这样你就可以知道它将与哪些其他指令竞争。 (例如,FMA (p01) 和 shuffle (p5) 根本不竞争 Haswell/Skylake 上的后端资源,仅竞争前端吞吐量。)也不要忘记测量前端 uop 计数: 一些指令解码以乘以 uops。
我们需要多少个不同的依赖链才能避免瓶颈?我们知道延迟(先测量它),并且我们知道最大可能的吞吐量(执行端口的数量,或前端吞吐量。)
例如,如果 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
写所有旗帜。 (参见 )
在 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
的标志合并微指令,我猜。BMI2 shlx edx, edx, ecx
正好是 1c,因为它只是一个微指令。)
相关:整个代码块(包含不同指令)的静态性能分析,参见What considerations go into predicting latency for operations on modern superscalar processors and how can I calculate them by hand?。 (它使用“延迟”这个词来表示整个计算的端到端延迟,但实际上询问的事情足够小,以至于 OoO exec 可以重叠不同的部分,因此指令延迟和吞吐量都很重要。)
load/store 的 Latency=2
数字似乎来自 Agner Fog 的指令表 (https://agner.org/optimize/)。不幸的是,它们对于 mov rax, [rax]
的链条并不准确。你会发现那是 4c
如果您通过将其置于循环中来测量延迟。
Agner 将 load/store 延迟分解为使总 store/reload 延迟正确的东西,但出于某种原因,他没有使负载部分等于 L1d 负载使用延迟当它来自缓存而不是存储缓冲区时。 (但还要注意,如果负载馈送 ALU 指令而不是另一条负载,则延迟为 5c。因此简单寻址模式快速路径仅有助于纯指针追逐。)
我正在寻找一种公式/方法来衡量一条指令的速度,或者更具体地说,通过 CPU 个周期给每条指令一个“分数”。
下面以汇编程序为例,
nop
mov eax,dword ptr [rbp+34h]
inc eax
mov dword ptr [rbp+34h],eax
以及以下英特尔 Skylake 信息:
mov r,m : Throughput=0.5 Latency=2
mov m,r : Throughput=1 Latency=2
nop : Throughput=0.25 Latency=non
inc : Throughput=0.25 Latency=1
我知道程序中指令的顺序在这里很重要但是 我希望创建一些不需要“精确到单个周期”的通用内容
有人知道我该怎么做吗?
没有可以应用的公式;你必须测量.
同一指令在同一 uarch 系列的不同版本上可能具有不同的性能。例如mulps
:
- 桑迪布里奇 1c / 5c throughput/latency.
- HSW 0.5 / 5. BDW 0.5 / 3(FMA 单元中的乘法路径更快?FMA 仍然是 5c)。
- SKL 0.5 / 4(低延迟 FMA)。 FMA 单元上的 SKL 运行s
addps
也放弃了专用的 FP 乘法单元,因此添加延迟更高,但吞吐量更高。
如果不进行测量或不了解一些微体系结构细节,您就无法预测其中的任何一个。我们预计 FP 数学运算不会是单周期延迟,因为它们比整数运算复杂得多。 (因此,如果它们是单周期,则时钟速度对于整数操作设置得太低。)
您可以通过在展开的循环中多次重复指令来进行测量。或者 完全 展开而没有循环,但是你打败了 uop-cache 并且可能会遇到前端瓶颈。 (例如解码 10 字节 mov r64, imm64
)
https://uops.info/ has already automated this testing for every form of every (unprivileged) instruction, and you can even click on any table entry to see what test loops they used. e.g. Skylake xchg r32, eax
latency testing (https://uops.info/html-lat/SKL/XCHG_R32_EAX-Measurements.html) from each input operand to each output. (2 cycle latency from EAX -> R8D, but 1 cycle latency from R8D -> EAX.) So we can guess that
https://uops.info/是目前最好的测试数据来源;当它和 Agner 的表格不一致时,我自己的测量 and/or 其他来源总是证实 uops.info 的测试是准确的。而且他们不会尝试像 movd xmm0,eax 和 back 那样为往返的两半组成延迟数,他们会向您展示可能的延迟范围,假设链的其余部分是最合理的。
Agner Fog 通过计时重复指令的大型非循环代码块来创建他的指令表(您似乎正在阅读)。 https://agner.org/optimize/。他的指令表的介绍部分简要说明了他如何测量,他的微架构指南详细说明了不同 x86 微架构的内部工作方式。不幸的是,在他手工编辑的表格中偶尔会出现拼写错误或 copy/paste 错误。
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
这个 7 inc
指令的依赖链将在每 7 * inc_latency
周期 1 次迭代时成为循环瓶颈。将 perf 计数器用于核心时钟周期(而不是 RDTSC 周期),您可以轻松测量 all 迭代的时间到 10k 中的 1 部分,并且可能比这更精确。 10000000 的重复计数隐藏了 start/stop 您使用的任何计时的开销。
我通常在 Linux 静态可执行文件中放置一个这样的循环,它只是直接进行 sys_exit(0)
系统调用(使用 syscall
)指令,并为整个可执行文件计时perf stat ./testloop
获取时间和周期计数。 (有关示例,请参见
另一个例子是lfence
来排除两个dep链的乱序执行window的复杂性。
为了测量吞吐量,您使用单独的寄存器,and/or偶尔包含一个异或归零来打破 dep 链并让无序的 exec 重叠。不要忘记也使用性能计数器来查看它可以 运行 在哪些端口上,这样你就可以知道它将与哪些其他指令竞争。 (例如,FMA (p01) 和 shuffle (p5) 根本不竞争 Haswell/Skylake 上的后端资源,仅竞争前端吞吐量。)也不要忘记测量前端 uop 计数: 一些指令解码以乘以 uops。
我们需要多少个不同的依赖链才能避免瓶颈?我们知道延迟(先测量它),并且我们知道最大可能的吞吐量(执行端口的数量,或前端吞吐量。)
例如,如果 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
写所有旗帜。 (参见
在 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
的标志合并微指令,我猜。BMI2 shlx edx, edx, ecx
正好是 1c,因为它只是一个微指令。)
相关:整个代码块(包含不同指令)的静态性能分析,参见What considerations go into predicting latency for operations on modern superscalar processors and how can I calculate them by hand?。 (它使用“延迟”这个词来表示整个计算的端到端延迟,但实际上询问的事情足够小,以至于 OoO exec 可以重叠不同的部分,因此指令延迟和吞吐量都很重要。)
load/store 的 Latency=2
数字似乎来自 Agner Fog 的指令表 (https://agner.org/optimize/)。不幸的是,它们对于 mov rax, [rax]
的链条并不准确。你会发现那是 4c
如果您通过将其置于循环中来测量延迟。
Agner 将 load/store 延迟分解为使总 store/reload 延迟正确的东西,但出于某种原因,他没有使负载部分等于 L1d 负载使用延迟当它来自缓存而不是存储缓冲区时。 (但还要注意,如果负载馈送 ALU 指令而不是另一条负载,则延迟为 5c。因此简单寻址模式快速路径仅有助于纯指针追逐。)