简述: 您可以使用以下方式覆盖此设置
mov eax, 1
(显式使用最佳操作数大小)
b8 01 00 00 00
mov rax, strict dword 1
(符号扩展的32位立即数)
48 c7 c0 01 00 00 00
mov rax, strict qword 1
(64位立即数,类似于AT&T语法中的movabs
)
48 b8 01 00 00 00 00 00 00 00
(同时mov rax, strict 1
等同于此,并且是禁用NASM优化时的结果。)
这是一种完全安全和有用的优化,类似于在编写
add eax, 1
时使用8位立即数而不是32位立即数。
NASM仅在较短的指令形式具有相同的架构效果时进行优化,因为
mov eax,1
隐式清零RAX的上32位。请注意,
add rax,0
与
add eax,0
不同,因此NASM无法进行优化:只有像
mov r32,...
/
mov r64,...
或
xor eax,eax
这样不依赖于32位vs. 64位寄存器旧值的指令才能以这种方式进行优化。(但NASM不会优化
xor rax,rax
或其他清零习惯用语;
您应始终手动使用32位操作数大小进行xor-zeroing。)
您可以使用
nasm -O1
来禁用它(默认为
-Ox
多次通行),但请注意,这种情况下您将获得10字节的
mov rax,strict qword 1
:显然NASM并不打算真正用于低于正常优化。没有设置可以使用最短的编码方式而不会改变反汇编结果(例如7字节的
mov rax,sign_extended_imm32
=
mov rax,strict dword 1
)。
-O0
和
-O1
之间的区别在于imm8与imm32,例如
add rax, 1
是使用
-O1
的
48 83 C0 01
(
add r/m64, sign_extended_imm8
),而使用
nasm -O0
的则是
48 05 01000000
(
add rax, sign_extended_imm32
)。
有趣的是,它仍然通过选择暗示RAX目标的特殊情况操作码进行了优化,而不是采用ModRM字节。不幸的是,
-O1
没有针对
mov
(其中sign_extended_imm8不可能)优化立即数大小。
如果您需要在某个地方使用特定的编码,请使用strict
而不是禁用优化来请求它。
其他汇编器
请注意,YASM不执行此操作数大小优化,因此,如果您关心代码大小(即使是间接地出于性能原因),在可以使用其他与NASM兼容的汇编器进行汇编的代码中,最好自己进行优化。
对于那些如果有非常大的(或负数)数字,32位和64位操作数大小不等效的指令,即使您使用NASM而不是YASM进行汇编,如果您想要获得大小/性能优势,仍需要显式使用32位操作数大小。
在x86-64中使用32位寄存器/指令的优点
GAS将使用-Os
进行此优化,例如gcc -Wa,-Os -c foo.S
,但不幸的是这不是默认设置。(gcc -O
选项不会影响传递给as
的选项,即使显式输入为.s
或.S
。如果您有任何内联汇编代码不确定是否手动优化,使用gcc -O3 -Wa,-Os foo.c
是一个好主意,假设它没有手动优化以使用更长的指令进行对齐。)
适合32位的常量,可以在64位中扩展为零或符号扩展
对于没有设置高位的32位常量,将它们扩展为64位时,无论是零扩展还是符号扩展都会得到相同的结果。因此,将mov rax, 1
组装成5字节mov r32, imm32
(隐式零扩展为64位),而不是7字节的mov r/m64, sign_extended_imm32
,这是一种纯粹的优化。
(有关x86-64允许的mov
形式的更多详细信息,请参见Difference between movq and movabsq in x86-64;AT&T语法有一个特殊名称用于10字节立即数形式,但NASM没有。)
性能
在所有当前的x86 CPU上,7字节编码和这种编码之间唯一的性能差异是代码大小,因此只有间接的影响如对齐和L1I$压力才是一个因素。内部只是一个mov-immediate,所以这个优化不会改变你的代码的微架构效果(当然除了代码大小/对齐/它如何打包在uop缓存中)。
10字节的mov r64,imm64编码对于代码大小来说更糟糕。如果常量实际上设置了其高位中的任何一位,则在Intel Sandybridge系列CPU上它会在uop缓存中产生额外的低效率(使用2个uop缓存条目,并可能需要额外的周期从uop缓存中读取)。但如果该常量在-2^31..+2^31范围内(有符号32位),即使它使用64位立即数编码在x86机器代码中,也可以使用单个uop缓存条目进行内部存储,而且效率相同。(参见
Agner Fog的微体系结构文档,
表9.1:Sandybridge部分中μop缓存中不同指令的大小)
从
如何将寄存器设置为零的多种方式?中,您可以强制使用以下三种编码之一:
mov eax, 1
mov rax, strict dword 1
mov rax, strict qword 1
请注意,NASM使用10字节编码(AT&T语法称为
movabs
,Intel语法模式下的
objdump
也是如此)来表示在汇编时未知但在链接时为常量的地址。
YASM选择
mov r64, imm32
,即它假定代码模型中的标签地址为32位,除非您使用
mov rsi, strict qword msg
。
YASM的行为通常很好(尽管像C编译器一样对于静态绝对地址使用
mov r32, imm32
会更好)。默认的非PIC代码模型将所有静态代码/数据放在虚拟地址空间的低2GiB中,因此零扩展或符号扩展的32位常量可以保存地址。
如果您想要64位标签地址,通常应使用
lea r64,[rel address]
进行RIP相对LEA。 (至少在Linux上,位置相关代码可以进入低32位,因此除非您使用大型/巨大的代码模型,在需要关注64位标签地址的任何时候,您还在制作PIC代码,应使用RIP相对LEA避免需要文本重定位的绝对地址常量)。
例如,gcc和其他编译器将使用
mov esi,msg
或
lea rsi,[rel msg]
而不是
mov rsi,msg
。
请参见
如何将函数或标签的地址加载到寄存器中。
mov rax,1
与mov eax,1
完全相同(因为在x86-64上,写入32位寄存器变体如eax
将自动清除64位rax
的上32位,这就是AMD设计x86-64的方式)。而且,eax
变体是用于小立即数的1B短操作码(rax
具有与REX前缀字节相同的操作码)。- 但我没有想到即使在这种情况下也会这样做,这让我有点惊讶(我只知道mov eax,1
会自动选择imm8
操作码变体,除非您编写mov eax,dword 1
以强制使用imm32
操作码变体)。 - Ped7gmov
没有 8 位扩展立即形式,你错了。 - ecm