作为对我的问题“在x86-64中使用32位寄存器/指令的优势”的跟进,我开始衡量指令的成本。我知道这已经做过多次(例如Agner Fog),但我只是为了好玩和自学而这样做。
我的测试代码非常简单(为了简化这里是伪代码,在实际汇编中):
for(outer_loop=0; outer_loop<NO;outer_loop++){
operation #first
operation #second
...
operation #NI-th
}
但是还有一些事情需要考虑。
- 如果循环的内部部分很大(large
NI>10^7
),则整个循环内容不适合存储在指令缓存中,因此必须反复加载,使得RAM的速度决定执行所需的时间。例如,对于大的内部部分,xorl %eax,%eax
(2字节)比xorq %rax,%rax
(3字节)快33%。 - 如果
NI
很小,整个循环轻松适合指令缓存,那么xorl %eax,%eax
和xorq %rax,%rax
的速度相同,可以每时钟周期执行4次。
然而,这个简单的模型并不适用于jmp
指令。对于jmp
指令,我的测试代码如下:
for(outer_loop=0; outer_loop<NO;outer_loop++){
jmp .L0
.L0: jmp .L1
L1: jmp L2
....
}
结果如下:
- 对于 “大” 的循环大小(即
NI>10^4
),我测量得到每个jmp
指令花费了 4.2 纳秒的时间(相当于从 RAM 加载 42 字节或在我的机器上约 12 个时钟周期)。 - 对于 “小” 的循环大小(
NI<10^3
),我测量得到每个jmp
指令花费了 1 纳秒的时间(大约为 3 个时钟周期,这听起来是合理的——Agner Fog 的表格显示了 2 个时钟周期的成本)。
jmp LX
指令使用 2 字节的 eb 00
编码。
因此,我的问题是:在“大”循环中,jmp
指令的高成本可能有什么解释?
附注:如果您想在自己的计算机上尝试它,可以从这里下载脚本,只需在src文件夹中运行 sh jmp_test.sh
命令。
编辑:实验结果确认了 Peter 的 BTB(分支目标缓冲器)大小理论。
下表显示了不同 NI
值的每个指令周期数(相对于 NI
=1000):
|oprations/ NI | 1000 | 2000| 3000| 4000| 5000| 10000|
|---------------------|------|------|------|------|------|------|
|jmp | 1.0 | 1.0 | 1.0 | 1.2 | 1.9 | 3.8|
|jmp+xor | 1.0 | 1.2 | 1.3 | 1.6 | 2.8 | 5.3|
|jmp+cmp+je (jump) | 1.0 | 1.5 | 4.0 | 4.4 | 5.5 | 5.5|
|jmp+cmp+je (no jump) | 1.0 | 1.2 | 1.3 | 1.5 | 3.8 | 7.6|
可以看出:
- 对于
jmp
指令,一个(尚未知名的)资源变得稀缺,这导致NI
大于4000时性能下降。 - 这个资源不会与
xor
等指令共享-如果在它们后面依次执行jmp
和xor
,性能下降仍然发生在NI
约为4000时。 - 但是,如果进行跳转,则此资源将与
je
共享-对于依次执行jmp
+je
,资源在NI
约为2000时会变得稀缺。 - 但是,如果
je
根本没有跳转,则该资源再次变得稀缺,NI
约为4000(第四行)。
Matt Godbolt的分支预测逆向工程文章表明,分支目标缓冲区的容量为4096个条目。 这是非常有力的证据,说明BTB错过是小型和大型jmp
循环之间观察到的吞吐量差异的原因。
xorq %rax,%rax
与xorl %eax,%eax
的作用完全相同,因此几乎没有理由使用前者(除非需要在某个地方插入nop
进行对齐)。 - fuzmov
和xor
,我需要在循环中执行 10^7 条指令才能看到“RAM 速度”。但是从 10^3 到 10^4,jmp
变慢了 4 倍。我不是说这是由于 RAM - 这是另一回事,但我不太清楚它是什么。 - eadjmp + cmp + je(无跳转)
情况之所以直到大约4,000次跳转才会出现资源稀缺,是因为未被执行的跳转不会消耗BTB条目(实际上,没有任何东西可以放入BTB中!)。 - BeeOnRope