所以,它并不一定是这样,这里有一些历史。尝试运行
mov rax, -1 # 0xFFFFFFFFFFFFFFFF
mov eax, 0
print rax
在您最喜欢的 x86 桌面上(print 取决于您的环境/语言/其他)。您会注意到,即使rax 开始时全是 1,并且您认为您只清除了底部的 32 位,print 语句打印为零!写入eax 完全擦除rax。为什么?这是非常奇怪和不直观的行为。原因很简单:因为它更快。当您继续写信给eax 时,试图保持rax 的较高值是绝对痛苦的。
然而,英特尔/AMD 在最初决定转向 32 位时并没有意识到这一点,并犯了一个致命错误,导致 al/ah 永远成为历史遗物:当你写信给al 或 ah,另一个不会被破坏!这确实更直观,这在 16 位时代曾经是一个好主意,因为现在你有两倍多的寄存器,并且你有一个 32 位寄存器!但是,如今,随着寄存器数量的增加,我们不再需要更多的寄存器了。我们真正想要的是更快的寄存器,并推动更多 GHz。从这个角度来看,每次你写信给al 或ah 时,处理器都需要保留另一半,这从根本上来说就是要贵得多。 (稍后解释原因)
理论说得够多了,让我们进行一些实际测试。每个测试用例测试了 3 次。这些测试是在 Intel Core i5-4278U CPU @ 2.60GHz
上运行的
仅 rax:1.067s、1.072s、1.097s
global _main
_main:
mov ecx, 1000000000
loop:
test ecx, ecx
jz exit
mov rax, 5
mov rax, 5
mov rax, 6
mov rax, 6
mov rax, 7
mov rax, 7
mov rax, 8
mov rax, 8
dec ecx
jmp loop
exit:
ret
只有 eax:1.072s、1.062s、1.060s
global _main
_main:
mov ecx, 1000000000
loop:
test ecx, ecx
jz exit
mov eax, 5
mov eax, 5
mov eax, 6
mov eax, 6
mov eax, 7
mov eax, 7
mov eax, 8
mov eax, 8
dec ecx
jmp loop
exit:
ret
只有啊:2.702s、2.748s、2.704s
global _main
_main:
mov ecx, 1000000000
loop:
test ecx, ecx
jz exit
mov ah, 5
mov ah, 5
mov ah, 6
mov ah, 6
mov ah, 7
mov ah, 7
mov ah, 8
mov ah, 8
dec ecx
jmp loop
exit:
ret
仅 ah/al:1.432s、1.457s、1.427s
global _main
_main:
mov ecx, 1000000000
loop:
test ecx, ecx
jz exit
mov ah, 5
mov al, 5
mov ah, 6
mov al, 6
mov ah, 7
mov al, 7
mov ah, 8
mov al, 8
dec ecx
jmp loop
exit:
ret
ah and al, 然后 eax: 1.117s, 1.084s, 1.082s
global _main
_main:
mov ecx, 1000000000
loop:
test ecx, ecx
jz exit
mov ah, 5
mov al, 5
mov eax, 6
mov al, 6
mov ah, 7
mov eax, 7
mov ah, 8
mov al, 8
dec ecx
jmp loop
exit:
ret
(请注意,这些测试与部分寄存器停顿无关,因为在写入 ah 后,我没有阅读 eax。参考主帖上的 cmets。)
从测试中可以看出,使用 al/ah 的速度要慢得多。使用 eax/rax 将其他时间吹出水面,并且 rax 和 eax 本身之间基本上没有性能差异。如前所述,原因是因为 eax/rax 直接覆盖了整个寄存器。但是,使用 ah 或 al 意味着需要维护另一半。
现在,如果您愿意,我们可以深入解释为什么每次使用时擦除寄存器更有效。从表面上看,这似乎并不重要,只需更新重要的部分,对吗?有什么大不了的?
嗯,现代 CPU 是智能的,它们会非常积极地并行化 CPU 知道不会相互干扰的操作,但前提是这种并行化实际上是可能的。例如,如果您将 eax 移动到 ebx,然后将 ebx 移动到 ecx,然后将 ecx 移动到 edx,那么 CPU 无法并行化它,并且运行速度会比平时慢。但是,如果您写入 eax、写入 ebx、写入 ecx 和写入 edx,那么 CPU 可以并行化所有这些操作,并且运行速度会比平时快得多!随意自行测试。
在内部,它的实现方式是立即开始执行和计算一条指令,即使早期的指令仍在执行中。但是,主要限制如下:
- 如果较早的指令写入到某个寄存器 A,而当前指令 读取从某个寄存器 A,则当前指令 必须等待直到前面的指令全部完成,这就是导致这种减速的原因。
在我们的mov eax, 5 垃圾邮件测试中,大约需要 1 秒,CPU 可以积极地并行运行所有操作,因为无论如何都不会读取任何指令,它们都是只写的。它只需要确保最近的写入是寄存器在任何未来读取期间保存的值(这很容易,因为即使操作都发生在重叠的时间段内,最后开始的操作也会完成最后一个)。
在mov ah, 5 垃圾邮件测试中,它比mov eax, 5 垃圾邮件测试慢了 2.7 倍,因为基本上没有简单的方法来并行化操作。每个操作都被标记为“从 eax 读取”,因为它依赖于 eax 的先前值,并且它也被标记为“写入 eax”,因为它修改了 eax 的值。如果一个操作必须从 eax 读取,它必须发生在 之前的操作完成写入 eax 之后。因此,并行化受到很大影响。
此外,如果您想自己尝试,您会注意到add eax, 5 垃圾邮件和add ah, 5 垃圾邮件都花费完全相同的时间(我的 CPU 上为 2.7 秒,与 mov ah, 5 完全相同!)。在这种情况下,add eax, 5 被标记为“从eax 读取”,并被标记为“写入eax”,因此它接收到与mov ah, 5 完全相同的减速,这也必须同时读取和写入eax!实际的 mov 与 add 无关紧要,逻辑门将在 ALU 的单个滴答中通过所需的操作立即将输入连接到输出。
所以,我希望能说明为什么 eax 的 64 位覆盖功能导致的时间比 ah 的保存系统快。
这里还有更多细节,为什么 ah/al 交换测试需要快得多的 1.43 秒?嗯,最有可能发生的事情是寄存器重命名有助于所有“mov ah, 5; mov al, 5”的写入。看起来 CPU 足够智能,可以拆分“ah”和“al”它们自己的完整 64 位寄存器,因为它们无论如何都使用“eax”寄存器的不同部分。因此,这允许并行进行一对连续的ah 然后al 操作,从而节省大量时间。如果“eax”被完整读取,CPU 需要将两个“al”与“ah”寄存器合并回一个寄存器,导致显着减速(稍后显示)。在早期的“mov ah, 5”-only 测试中,不可能将 eax 拆分为单独的寄存器,因为无论如何我们每次都使用“ah”。
而且,有趣的是,如果您查看 ah/al/eax 测试,您会发现它几乎与 eax 测试一样快!在这种情况下,我预测所有三个都有自己的寄存器,因此代码非常并行化。
当然,如前所述,当必须合并 ah/al 时,尝试在该循环中的任何位置读取 eax 会降低性能,这是一个示例:
时间:3.412s、3.390s、3.515s
global _main
_main:
mov ecx, 1000000000
loop:
test ecx, ecx
jz exit
mov ah, 5
mov al, 5
xor eax, 5
mov al, 6
mov ah, 8
xor eax, 5
mov al, 8
dec ecx
jmp loop
exit:
ret
但是,请注意,上面的测试没有适当的控制组,因为它使用 xor 而不是 mov(例如,如果只使用“xor”是为什么它很慢的原因)。所以,这里有一个测试来比较它:
时间:1.426s、1.424s、1.392s
global _main
_main:
mov ecx, 1000000000
loop:
test ecx, ecx
jz exit
mov ah, 5
mov al, 5
xor ah, 5
mov al, 6
mov ah, 8
xor ah, 5
mov al, 8
dec ecx
jmp loop
exit:
ret
上述测试非常积极地合并,导致可怕的 3.4 秒,实际上比任何其他测试都慢得多。但是,al/ah 测试将 al/ah 拆分为两个不同的寄存器,因此运行速度非常快,比仅使用 ah 更快,因为可以并行化连续的 ah/al 操作。因此,这是英特尔愿意做出的权衡。
如前所述,正如所见,您是否执行xor vs add vs mov 并不重要,上面的 ah/al 仍然需要 1.4 秒,直接按位/添加/移动全部用很少的逻辑门将输入连接到输出,你使用哪种操作并不重要(但是, mul 和 div 确实会更慢,这需要更严格的计算,因此需要几个微周期)。
过去的两个测试显示报告的部分寄存器停顿,老实说,我一开始甚至没有考虑过。我首先认为寄存器重命名将有助于缓解问题,他们似乎在 ah/al 混合和 ah/al/eax 混合中这样做。然而,用脏的 ah/al 值读取 eax 是很残酷的,因为处理器现在必须组合 ah/al 寄存器。看起来处理器制造商认为寄存器重命名部分寄存器仍然值得,这是有道理的,因为大多数使用 ah/al 的工作不涉及读取 eax,如果这是您的计划,您只需从 ah/al 读取。这样,与 ah/al 打交道的紧密循环会大有裨益,唯一的危害是下次使用 eax 时会打嗝(此时可能不再使用 ah/al)。
如果英特尔想要,而不是 ah/al 寄存器重命名优化给出 1.4 秒,正常 ah 是 2.7 秒,寄存器合并滥用需要 3.4 秒,英特尔可能不会关心寄存器重命名,所有这些测试都会完全相同的 2.7 秒。但是,英特尔很聪明,他们知道那里有很多代码会想要使用 ah 和 al,但是找到大量使用 al 和 ah 的代码并不常见,同时还一直从总 eax 中读取嗯。
总体而言,即使在没有部分寄存器停顿的情况下,写入 ah 仍然比写入 eax 慢得多,这是我试图解决的问题。
当然,结果可能会有所不同。其他处理器(很可能是非常旧的处理器)可能具有关闭一半总线的控制位,这将允许总线在需要时像 16 位或 8 位总线一样工作。这些控制位必须通过沿输入的逻辑门连接到寄存器,这会稍微减慢寄存器的任何和所有使用,因为现在在寄存器可以更新之前还要通过一个门。由于此类控制位在绝大多数情况下都会关闭(因为很少会混淆 8 位/16 位值),看起来英特尔决定不这样做(有充分的理由)。