ChatGPT解决这个技术问题 Extra ChatGPT

为什么 32 位寄存器上的 x86-64 指令会将完整 64 位寄存器的上部归零?

x86-64 Tour of Intel Manuals 中,我读到

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

同一来源引用的英特尔文档(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 汇编的怪癖)。

如果你用谷歌搜索“部分寄存器停顿”,你会发现很多关于他们(几乎可以肯定)试图避免的问题的信息。
不只是“最”。 AFAIK,具有 r32 目标操作数的 all 指令将高位 32 归零,而不是合并。例如,一些汇编程序会将 pmovmskb r64, xmm 替换为 pmovmskb r32, xmm,从而节省 REX,因为 64 位目标版本的行为相同。即使 Operation section of the manual 分别列出了 32/64 位 dest 和 64/128/256b 源的所有 6 种组合,r32 形式的隐式零扩展复制了 r64 形式的显式零扩展。我对硬件实现很好奇......
@HansPassant,循环引用开始。

P
Peter Cordes

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

这样,您可以在 64 位模式下使用 32 位值编写快速代码,而不必一直显式地破坏依赖关系。如果没有这种行为,64 位模式下的每条 32 位指令都必须等待之前发生的事情,即使几乎永远不会使用高位部分。 (将 int 设为 64 位会浪费缓存占用空间和内存带宽;x86-64 most efficiently supports 32 and 64-bit operand sizes

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

另请参阅 Why doesn't GCC use partial registers?,了解实际 CPU 如何处理对 8 位和 16 位部分寄存器的写入(以及随后对完整寄存器的读取)。


我不认为这很奇怪,我认为他们不想破坏太多并保留旧的行为。
@Alex 当他们引入 32 位模式时,高部分没有旧行为。之前没有高的部分。当然之后就不能再改了。
我将您的“16 位指令的行为是奇怪的”解释为“奇怪的是,在 64 位模式下 16 位操作数不会发生零扩展”。因此,我对在 64 位模式下保持相同方式以实现更好的兼容性的评论。
@Alex 哦,我明白了。好的。从这个角度来看,我认为这并不奇怪。只是从“回首过去,也许这不是一个好主意”的角度来看。我想我应该更清楚:)
16 位命令的逻辑可以是“如果我们必须保持兼容性,因此依赖于先前寄存器值的位 16-31,清除位 32-63 不会拯救我们。所以,完全忽略此清除。”无论如何,这不是最奇怪的 x86-64。
B
Bo Persson

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

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

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


如果这是正确的,那么符号扩展而不是 0 扩展不是更有意义吗?
符号扩展速度较慢,即使在硬件中也是如此。零扩展可以与产生下半部分的任何计算并行完成,但在计算下半部分(至少是符号)之前不能进行符号扩展。
另一个相关的技巧是使用 XOR EAX, EAX,因为 XOR RAX, RAX 需要一个 REX 前缀。
@Nubok:当然,他们可以添加 movzx / movsx 的编码,该编码需要立即参数。大多数情况下,将高位清零更方便,因此您可以将值用作数组索引(因为有效地址中的所有 reg 必须具有相同的大小:[rsi + edx] 是'不允许)。当然,避免错误的依赖/部分注册停顿(另一个答案)是另一个主要原因。
并且更改那里的规则也会破坏旧代码。旧代码无论如何都不能在 64 位模式下运行(例如,1 字节的 inc/dec 是 REX 前缀);这无关紧要。不清理 x86 缺陷的原因是长模式和兼容/传统模式之间的差异较小,因此更少的指令必须根据模式进行不同的解码。 AMD 不知道 AMD64 会流行起来,不幸的是它非常保守,因此需要更少的晶体管来支持。从长远来看,如果编译器和人类必须记住哪些东西在 64 位模式下的工作方式不同,那就太好了。
L
Lewis Kelsey

如果不将零扩展到 64 位,则意味着从 rax 读取的指令对其 rax 操作数(写入 eax 的指令和之前写入 rax 的指令)有 2 个依赖项,这会导致 partial register stall,当有 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, rax 中,rax 只需要等待它的单个依赖项,这意味着它可以更快地开始执行并退出,从而释放执行单元。此外,它还允许更有效的零习惯用法,如 xor eax, eax 到零 rax,而不需要 REX 字节。


Skylake 上的部分标志至少可以通过为 CF 和任何 SPAZO 提供单独的输入来工作。 (所以 cmovbe 是 2 微秒,但 cmovb 是 1)。但是没有任何 CPU 会按照您的建议进行任何部分寄存器重命名。相反,如果部分 reg 与完整 reg 分开重命名(即“脏”),它们会插入一个合并 uop。请参阅 Why doesn't GCC use partial registers?How exactly do partial registers on Haswell/Skylake perform? Writing AL seems to have a false dependency on RAX, and AH is inconsistent
P6 系列 CPU 要么停顿约 3 个周期以插入合并微指令(Core2 / Nehalem),要么更早的 P6 系列(PM、PIII、PII、PPro)仅停顿(至少?)约 6 个周期。也许这就像您在 2 中建议的那样,等待完整的 reg 值通过写回永久/架构寄存器文件可用。
@PeterCordes 哦,我知道至少为部分标志位合并 uops。有道理,但我忘记了它是如何工作的;它点击了一次,但我忘了做笔记
@PeterCordes microarchitecture.pdf:This gives a delay of 5 - 6 clocks. The reason is that a temporary register has been assigned to AL to make it independent of AH. The execution unit has to wait until the write to AL has retired before it is possible to combine the value from AL with the value of the rest of EAX 我找不到可以用来解决这个问题的“合并 uop”示例,对于部分标志停止也是如此
对,早期的 P6 只是停滞不前,直到写回。 Core2 和 Nehalem 在之后/之前插入合并 uop?只会让前端停滞更短的时间。 Sandybridge 插入合并微指令而不会停止。 (但是 AH 合并必须自己在一个循环中发出,而 AL 合并可以是一个完整组的一部分。)Haswell/SKL 根本不会将 AL 与 RAX 分开重命名,因此 mov al, [mem] 是一个微融合负载+ALU 合并,仅重命名 AH,AH 合并 uop 仍然单独发布。这些 CPU 中的部分标志合并机制各不相同,例如 Core2/Nehalem 仍然只是为部分标志停止,这与部分注册不同。
s
supercat

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

当 80386 添加 32 位寄存器时,没有提供仅访问寄存器的上半部分的工具,但是像 ROR ESI,16 这样的指令将足够快,因此仍然可以保存两个 16 位值在 ESI 中并在它们之间切换。

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

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

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


隐式清零高位也使 5 字节 mov eax, 1 (opcode + imm32) 用作设置完整 64 位寄存器的一种方式,而不是需要 7 字节 mov rax, sign_extended_imm32 (REX + opcode + modrm + imm32) 或 10 -byte mov rax, imm64 (rex + opcode + imm64)。在许多其他情况下,零扩展很有用,例如,当使用无符号 32 位整数作为数组索引(寻址模式的一部分)或已知为非负的有符号整数时。
因此,即使除了错误依赖的性能问题之外,您想要清除高垃圾比合并某些东西更频繁。 x86-64 可能有一个 movzx r64, r/m32 ,你每次需要时都必须使用它,但这会更糟。尤其是如果他们希望像普通 C 类型模型(32 位 int、64 位指针)那样使用 32 位整数仍然有效。相关:MOVZX missing 32 bit register to 64 bit register - 一些像 MIPS64 这样的 ISA 做出了不同的选择,比如保持窄值符号扩展。
@PeterCordes:很多其他答案都提到了寄存器重命名,但我认为不熟悉这个概念的人可以从更完整的示例中受益。从硬件复杂性或指令集可用性的角度来看,我认为有一个前缀可以促进例如“add rax,signed byte[whatever]”或“add rsi,unsigned word[whatever]”并不困难]”,并且指令大小对性能的影响,在大多数情况下,几乎没有。真正的问题是跟踪额外的依赖关系是昂贵的。顺便提一句...
...我有时想知道拥有一个“通用”ABI 是否有意义,它根据预期的调用约定使用修改后的符号名称作为入口点。如果一个入口点仅在用于传递小于 64 位参数的所有寄存器都已知对其类型进行适当扩展时使用,那么编译器可以在它知道所有参数寄存器的情况下使用该入口点已经适当地设置了,并且当调用者无法保证时,该入口点将根据需要使用零或符号扩展值。
是的,这是隐式扩展避免的错误依赖问题的一个很好的清晰而具体的例子。已经投票了。是的,如果他们想将所有内容扩展到 64 位,他们可能会将其他一些已删除的操作码(如 AAA / AAM / 等)重新用作 64 位模式源大小/签名覆盖。 (但这会使像 imul 这样的指令在 K8 上变慢(64 位乘法不如 32 快),除非这也设置了操作数大小并将结果从 32 位截断/扩展以填充 reg .) 但是这里的评论不是进一步讨论的地方:/

关注公众号,不定期副业成功案例分享
关注公众号

不定期副业成功案例分享

领先一步获取最新的外包任务吗?

立即订阅