155

x86-64 Tour of Intel Manuals中,我阅读了

也许最令人惊讶的事实是,诸如MOV EAX, EBX自动将寄存器的高 32 位归零之类的指令RAX

同一来源引用的英特尔文档(3.4.1.1 General-Purpose Registers in 64-Bit Mode in manual Basic Architecture)告诉我们:

  • 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 汇编的怪癖)。

4

4 回答 4

120

我不是 AMD 或为他们说话,但我会以同样的方式做到这一点。因为将高半部分归零不会产生对先前值的依赖,所以 CPU 将不得不等待。如果不这样做,寄存器重命名机制基本上会被打败

这样,您可以在 64 位模式下使用 32 位值编写快速代码,而不必一直显式地破坏依赖关系。如果没有这种行为,64 位模式下的每条 32 位指令都必须等待之前发生的事情,即使几乎永远不会使用高位部分。(制作int64 位会浪费缓存占用空间和内存带宽;x86-64 最有效地支持 32 位和 64 位操作数大小

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


另请参阅为什么 GCC 不使用部分寄存器?有关实际 CPU 如何处理对 8 位和 16 位部分寄存器的写入(以及随后读取完整寄存器)的实际细节。

于 2012-06-24T11:53:08.200 回答
12

它只是节省了指令和指令集的空间。您可以使用现有(32 位)指令将小的立即数移动到 64 位寄存器。

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

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

于 2012-06-24T11:50:14.067 回答
5

如果不将零扩展到 64 位,则意味着读取的指令rax对其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,例如,如果它最初包含 0xffffffffffffffff,则结果将是 0xffffffff00000007,但 ISA 几乎没有理由以这样的代价做出此保证,并且更有可能实际上需要更多零扩展的好处,因此它节省了额外的代码行mov rax, 0。通过保证它总是零扩展到 64 位,编译器可以在考虑到这个公理的同时在 中工作mov rdx, raxrax只需要等待它的单个依赖项,这意味着它可以更快地开始执行并退出,从而释放执行单元。此外,它还允许更有效的零习语,如xor eax, eaxrax,而不需要 REX 字节。

于 2020-03-31T19:20:08.923 回答
2

从硬件的角度来看,更新半个寄存器的能力总是有些昂贵,但在最初的 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也很困难,因为处理器必须跟踪这样一个事实,即虽然下半部分可用,但上半部分不可用。

于 2021-04-26T18:59:33.537 回答