为什么32位寄存器上的 x86-64指令为整个64位寄存器的上半部分0?

X86-64英特尔手册之旅里,我读到

也许最令人惊讶的事实是,像 MOV EAX, EBX这样的指令会自动将 RAX寄存器的上32位归零。

同一来源引用的 Intel 文档(3.4.1.1通用寄存器在手动基本架构中的64位模式)告诉我们:

  • 64位操作数在目标通用寄存器中生成64位结果。
  • 32位操作数生成一个32位结果,在目标通用寄存器中从零扩展到64位结果。
  • 8位和16位操作数生成8位或16位结果。目标通用寄存器的上56位或48位(分别)不会被操作修改。如果8位或16位操作的结果用于64位地址计算,则显式地将寄存器符号扩展到完整的64位。

在 x86-32和 x86-64汇编中,有16位指令,如

mov ax, bx

不要表现出这种“奇怪”的行为,即 eax 的上面一个单词被归零。

因此: 引入这种行为的原因是什么?乍一看,这似乎不合逻辑(但原因可能是我已经习惯了 x86-32汇编的怪异之处)。

41745 次浏览

它只是节省了指令和指令集中的空间。通过使用现有的(32位)指令,可以将小的即时值移动到64位寄存器。

MOV EAX, 42可以重用时,它还可以使您不必为 MOV RAX, 42编码8个字节的值。

这种优化对于8位和16位操作来说不那么重要(因为它们更小) ,并且改变那里的规则也会破坏旧的代码。

我不是 AMD,也不是他们的代言人,但我也会这么做。因为对高半部分进行归零并不会创建对前一个值的依赖关系,所以 CPU 将不得不等待。如果不这样做,寄存器重命名机制基本上就会失败。

通过这种方式,您可以在64位模式下使用32位值编写快速代码,而不必一直显式地打破依赖关系。如果没有这种行为,64位模式下的每一条32位指令都将不得不等待以前发生过的事情,即使那个高位几乎永远不会被使用。(使用 int64位将浪费缓存占用和内存带宽; X86-64最有效地支持32位和64位操作数大小)

8位和16位操作数大小的行为是奇怪的。依赖性疯狂是现在避免使用16位指令的原因之一。X86-64从8086(8位)和386(16位)继承了这一点,并决定让8位和16位寄存器在64位模式下的工作方式与在32位模式下的工作方式相同。


有关实际 CPU 如何处理写入8位和16位部分寄存器(以及随后读取完整寄存器)的详细信息,请参阅 为什么海湾合作委员会不使用部分寄存器?

如果从 rax读取的指令没有扩展到64位,那就意味着它的 rax操作数(写入 eax的指令和写入之前的 rax的指令)将有2个依赖项,这将导致 局部登记失速,当有3个可能的宽度时,这将开始变得棘手,所以它有助于 raxeax写入完整的寄存器,这意味着64位指令集不会引入任何新的部分重命名层。

mov rdx, 1
mov rax, 6
imul rax, rdx
mov rbx, rax
mov eax, 7 //retires before add rax, 6
mov rdx, rax // has to wait for both imul rax, rdx and mov eax, 7 to finish before dispatch to the execution units, even though the higher order bits are identical anyway

不为零扩展的唯一好处是确保包含 rax的高阶位,例如,如果它最初包含0xffffffffffff,结果将是0xffffffff00000007,但是 ISA 没有什么理由以这样的代价来做出这样的保证,而且更有可能的是,实际上需要更多的零扩展的好处,所以它节省了额外的代码 mov rax, 0行。通过保证它总是零扩展到64位,编译器可以按照这个公理工作,而在 mov rdx, rax中,rax只需要等待它的单个依赖项,这意味着它可以更快地开始执行并退出,从而释放执行单元。此外,它还允许更有效的零习惯用法,如 xor eax, eax到零 rax,而不需要 REX 字节。

从硬件角度来看,更新半个寄存器的能力总是有些昂贵,但是在最初的8088上,允许手写的汇编代码将8088视为具有两个非堆栈相关的16位寄存器和八个8位寄存器、六个非堆栈相关的16位寄存器和零个8位寄存器,或者其他16位和8位寄存器的中间组合是有用的。这样有用的东西值得付出额外的代价。

当80386添加了32位寄存器时,没有提供任何设施只能访问寄存器的上半部分,但是像 ROR ESI,16这样的指令速度足够快,因此在 ESI 中保存两个16位值并在它们之间切换仍然是有价值的。

随着向 x64体系结构的迁移,增加的寄存器集和其他体系结构增强减少了程序员将最大数量的信息压缩到每个寄存器中的需要。此外,寄存器重命名增加了部分注册表更新的成本。如果代码执行以下操作:

    mov rax,[whatever]
mov [something],rax
mov rax,[somethingElse]
mov [yetAnother],rax

寄存器重命名和相关逻辑可以让 CPU 记录下这样一个事实: 从 [whatever]加载的值需要写入到 something,然后——只要最后两个地址不同——允许加载的 somethingElse和存储到 yetAnother进行处理,而不必等待从 whatever实际读取的数据。然而,如果第三条指令是 mov eax,[somethingElse,并且指定保留上面的比特不受影响,那么第四条指令在第一次加载完成之前不能存储 RAX,甚至连允许 EAX的加载都很困难,因为处理器必须跟踪这样一个事实: 虽然下半部分可用,但是上半部分不可用。