在 x86-64 中使用 32bit registers/instructions 的优点
The advantages of using 32bit registers/instructions in x86-64
有时 gcc 使用 32 位寄存器,而我希望它使用 64 位寄存器。例如下面的 C 代码:
unsigned long long
div(unsigned long long a, unsigned long long b){
return a/b;
}
使用 -O2 选项编译为(省略一些样板内容):
div:
movq %rdi, %rax
xorl %edx, %edx
divq %rsi
ret
对于无符号除法,寄存器%rdx
需要为0
。这个可以通过xorq %rdx, %rdx
来实现,但是xorl %edx, %edx
好像也有同样的效果。
至少在我的机器上 xorl
比 xorq
没有性能提升(即加速)。
其实我的问题不止一个:
- 为什么 gcc 更喜欢 32 位版本?
- 为什么 gcc 停在
xorl
而没有使用 xorw
?
- 有机器
xorl
比 xorq
快吗?
- 如果可能,是否应该总是更喜欢 32 位 register/operations 而不是 64 位 register/operations?
在 64 位模式下,写入 32 位寄存器会将高 32 位清零 =>
xorl %edx, %edx
将 rdx
的上部归零为 "free"。
另一方面,xor %rdx, %rdx
是用一个额外的字节编码的,因为它需要一个 REX 前缀。
尝试将 64 位寄存器置零时,将其作为 32 位寄存器进行异或显然是一个胜利。
Why does gcc prefer the 32bit version?
主要是代码大小:机器代码编码中不需要 REX 前缀。
Why does gcc stop at xorl
and doesn't use xorw
?
写入 8 位或 16 位部分寄存器不会零扩展到寄存器的其余部分。 (Only writing a 32-bit register implicitly zero-extends to 64)
此外,xorw
需要操作数大小前缀进行编码,因此它的大小与 xorq
相同,大于 xorl
。 32 位操作数大小是 x86-64 机器代码中的默认值,不需要前缀。(对于大多数指令;一些像 push
/pop
和call
/jmp
默认为 64 位,包括内存间接 call [rdi]
= ff 17
和内存中的指针。)8 位操作数大小使用单独的操作码,而不是前缀, 但仍然可能有部分注册惩罚。
另见 32 位寄存器 不是 考虑的部分寄存器,因为写入它们总是写入整个 64 位寄存器。 (主要问题是写入部分寄存器,而不是在全角写入后读取它们。)
Are there machines for which xorl is faster than xorq?
是的,Silvermont / KNL 只识别具有 32 位操作数大小的 (依赖关系破坏和其他好东西)。因此,即使代码大小相同,xor %r10d, %r10d
也比 xor %r10, %r10
好得多。 (xor
需要 r10
的 REX 前缀,无论操作数大小如何)。
在所有 CPU 上,代码大小总是对解码和 I-cache 占用空间有潜在影响(除非后面的 .p2align
指令如果前面的指令只会产生更多填充代码更小1)。使用 32 位操作数大小进行异或归零(或一般隐式零扩展而不是显式 2,包括使用 .)[=54 没有任何缺点=]
大多数指令对于所有操作数大小都是相同的速度,因为现代 x86 CPU 可以负担得起宽 ALU 的晶体管预算。例外情况包括 ,并且 64 位 div
在所有 CPU 上都明显较慢。 AMD pre-Ryzen 速度较慢 popcnt r64
。 Atom/Silvermont 比 r32
慢 shld/shrd r64
。主流 Intel(Skylake 等)速度较慢 bswap r64
。
Should one always prefer 32bit register/operations if possible rather than 64bit register/operations?
是的,至少出于代码大小的原因,更喜欢 32 位操作,但请注意,在指令中的任何位置(包括寻址模式)使用 r8..r15 也会需要一个 REX 前缀。因此,如果您有一些数据,您可以使用 32 位操作数大小(或指向 8/16/32 位数据的指针),更愿意将其保存在低 8 位命名寄存器中(e/rax..)比高 8 编号的寄存器。
但是不要花费额外的指令来实现它;节省几个字节的代码大小通常是最不重要的考虑因素。 例如只需使用 r8d
而不是 saving/restoring rbx
因此如果您需要一个不必保留调用的额外寄存器,则可以使用 ebx
。使用 32 位 r8d
而不是 64 位 r8
对代码大小没有帮助,但对于某些 CPU 上的某些操作来说它可以更快(见上文)。
这也适用于您只关心寄存器的低 16 位的情况,。
另请参阅 http://agner.org/optimize/ and the x86 标签 wiki。
脚注 1:很少有使指令长于必要的用例 ()
无需 NOP 对齐后面的分支目标。
针对特定微体系结构的前端进行调优(即通过控制指令边界的位置来优化解码)。插入 NOP 会消耗额外的前端带宽并完全破坏整个目的。
汇编程序不会为您做这些,并且每次更改任何内容时都需要重新进行手动操作非常耗时(并且您可能必须使用 .byte
指令来手动编码指令) .
脚注 2:我发现隐式零扩展至少与更广泛的操作一样便宜的规则有一个例外:Haswell/Skylake AVX 128-与 128 位指令相比,256 位指令读取的位加载具有额外的 1c 存储转发延迟。 (详情in a thread on Agner Fog's blog forum。)
有时 gcc 使用 32 位寄存器,而我希望它使用 64 位寄存器。例如下面的 C 代码:
unsigned long long
div(unsigned long long a, unsigned long long b){
return a/b;
}
使用 -O2 选项编译为(省略一些样板内容):
div:
movq %rdi, %rax
xorl %edx, %edx
divq %rsi
ret
对于无符号除法,寄存器%rdx
需要为0
。这个可以通过xorq %rdx, %rdx
来实现,但是xorl %edx, %edx
好像也有同样的效果。
至少在我的机器上 xorl
比 xorq
没有性能提升(即加速)。
其实我的问题不止一个:
- 为什么 gcc 更喜欢 32 位版本?
- 为什么 gcc 停在
xorl
而没有使用xorw
? - 有机器
xorl
比xorq
快吗? - 如果可能,是否应该总是更喜欢 32 位 register/operations 而不是 64 位 register/operations?
在 64 位模式下,写入 32 位寄存器会将高 32 位清零 =>
xorl %edx, %edx
将 rdx
的上部归零为 "free"。
另一方面,xor %rdx, %rdx
是用一个额外的字节编码的,因为它需要一个 REX 前缀。
尝试将 64 位寄存器置零时,将其作为 32 位寄存器进行异或显然是一个胜利。
Why does gcc prefer the 32bit version?
主要是代码大小:机器代码编码中不需要 REX 前缀。
Why does gcc stop at
xorl
and doesn't usexorw
?
写入 8 位或 16 位部分寄存器不会零扩展到寄存器的其余部分。 (Only writing a 32-bit register implicitly zero-extends to 64)
此外,xorw
需要操作数大小前缀进行编码,因此它的大小与 xorq
相同,大于 xorl
。 32 位操作数大小是 x86-64 机器代码中的默认值,不需要前缀。(对于大多数指令;一些像 push
/pop
和call
/jmp
默认为 64 位,包括内存间接 call [rdi]
= ff 17
和内存中的指针。)8 位操作数大小使用单独的操作码,而不是前缀, 但仍然可能有部分注册惩罚。
另见
Are there machines for which xorl is faster than xorq?
是的,Silvermont / KNL 只识别具有 32 位操作数大小的 xor %r10d, %r10d
也比 xor %r10, %r10
好得多。 (xor
需要 r10
的 REX 前缀,无论操作数大小如何)。
在所有 CPU 上,代码大小总是对解码和 I-cache 占用空间有潜在影响(除非后面的 大多数指令对于所有操作数大小都是相同的速度,因为现代 x86 CPU 可以负担得起宽 ALU 的晶体管预算。例外情况包括 Should one always prefer 32bit register/operations if possible rather than 64bit register/operations? 是的,至少出于代码大小的原因,更喜欢 32 位操作,但请注意,在指令中的任何位置(包括寻址模式)使用 r8..r15 也会需要一个 REX 前缀。因此,如果您有一些数据,您可以使用 32 位操作数大小(或指向 8/16/32 位数据的指针),更愿意将其保存在低 8 位命名寄存器中(e/rax..)比高 8 编号的寄存器。 但是不要花费额外的指令来实现它;节省几个字节的代码大小通常是最不重要的考虑因素。 例如只需使用 这也适用于您只关心寄存器的低 16 位的情况, 另请参阅 http://agner.org/optimize/ and the x86 标签 wiki。 脚注 1:很少有使指令长于必要的用例 ( 无需 NOP 对齐后面的分支目标。 针对特定微体系结构的前端进行调优(即通过控制指令边界的位置来优化解码)。插入 NOP 会消耗额外的前端带宽并完全破坏整个目的。 汇编程序不会为您做这些,并且每次更改任何内容时都需要重新进行手动操作非常耗时(并且您可能必须使用 脚注 2:我发现隐式零扩展至少与更广泛的操作一样便宜的规则有一个例外:Haswell/Skylake AVX 128-与 128 位指令相比,256 位指令读取的位加载具有额外的 1c 存储转发延迟。 (详情in a thread on Agner Fog's blog forum。).p2align
指令如果前面的指令只会产生更多填充代码更小1)。使用 32 位操作数大小进行异或归零(或一般隐式零扩展而不是显式 2,包括使用 div
在所有 CPU 上都明显较慢。 AMD pre-Ryzen 速度较慢 popcnt r64
。 Atom/Silvermont 比 r32
慢 shld/shrd r64
。主流 Intel(Skylake 等)速度较慢 bswap r64
。
r8d
而不是 saving/restoring rbx
因此如果您需要一个不必保留调用的额外寄存器,则可以使用 ebx
。使用 32 位 r8d
而不是 64 位 r8
对代码大小没有帮助,但对于某些 CPU 上的某些操作来说它可以更快(见上文)。
.byte
指令来手动编码指令) .