在 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 汇编的怪癖)。
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 形式的显式零扩展。我对硬件实现很好奇......
xor eax,eax
or xor r8d,r8d
is the best way to zero RAX or R8(为 RAX 保存 REX 前缀,并且在 Silvermont 上甚至没有专门处理 64 位 XOR)。相关:How exactly do partial registers on Haswell/Skylake perform? Writing AL seems to have a false dependency on RAX, and AH is inconsistent
我不是 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 位部分寄存器的写入(以及随后对完整寄存器的读取)。
它只是节省了指令和指令集的空间。您可以使用现有(32 位)指令将小的立即数移动到 64 位寄存器。
当可以重用 MOV EAX, 42
时,它还使您不必为 MOV RAX, 42
编码 8 字节值。
这种优化对于 8 位和 16 位操作并不那么重要(因为它们更小),并且更改那里的规则也会破坏旧代码。
XOR EAX, EAX
,因为 XOR RAX, RAX
需要一个 REX 前缀。
[rsi + edx]
是'不允许)。当然,避免错误的依赖/部分注册停顿(另一个答案)是另一个主要原因。
如果不将零扩展到 64 位,则意味着从 rax
读取的指令对其 rax
操作数(写入 eax
的指令和之前写入 rax
的指令)有 2 个依赖项,这会导致 partial register stall,当有 3 种可能的宽度时它开始变得棘手,因此它有助于 rax
和 eax
写入完整的寄存器,这意味着 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 字节。
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
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”示例,对于部分标志停止也是如此
mov al, [mem]
是一个微融合负载+ALU 合并,仅重命名 AH,AH 合并 uop 仍然单独发布。这些 CPU 中的部分标志合并机制各不相同,例如 Core2/Nehalem 仍然只是为部分标志停止,这与部分注册不同。
从硬件的角度来看,更新半个寄存器的能力总是有些昂贵,但在最初的 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
的加载也会这很困难,因为处理器必须跟踪这样一个事实,即虽然下半部分可用,但上半部分不可用。
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 位整数作为数组索引(寻址模式的一部分)或已知为非负的有符号整数时。
int
、64 位指针)那样使用 32 位整数仍然有效。相关:MOVZX missing 32 bit register to 64 bit register - 一些像 MIPS64 这样的 ISA 做出了不同的选择,比如保持窄值符号扩展。
imul
这样的指令在 K8 上变慢(64 位乘法不如 32 快),除非这也设置了操作数大小并将结果从 32 位截断/扩展以填充 reg .) 但是这里的评论不是进一步讨论的地方:/
不定期副业成功案例分享