这是复制(转换)未签名寄存器的指令:http : //www.felixcloutier.com/x86/MOVZX.html
基本上,该指令具有8-> 16、8-> 32、8-> 64、16-> 32和16-> 64。
32-> 64转换在哪里?我需要为此使用签名版本吗?
如果是这样,如何将全64位用于无符号整数?
使用mov eax, edi
以零扩展到EDI RAX如果您已经不能保证RDI的高位均为零。请参阅:为什么32位寄存器上的x86-64指令将整个64位寄存器的上半部分归零?
最好使用不同的源/目标寄存器,因为在Intel和AMD CPU上,消除运动均失败mov eax,eax
。当转移到不同的寄存器时,不需要任何执行单元就可以实现零延迟。(gcc显然不知道这一点,通常零扩展到位。)但是,不要花费额外的指令来实现这一目标。
摘要:movzx和movsx的每个不同的源宽度都需要一个不同的操作码。目标宽度由前缀控制。由于mov
可以完成这项工作,因此新的操作码movzx dst, r/m32
将是多余的。
在设计AMD64汇编器语法时,AMD选择不将其movzx rax, edx
作为的伪指令mov eax, edx
。这可能是一件好事,因为知道编写32位寄存器会将高字节清零对于为x86-64编写高效代码非常重要。
AMD64确实需要使用32位源操作数进行符号扩展的新操作码。他们movsxd
出于某种原因命名了助记符,而不是使其成为movsx
助记符的第3个操作码。英特尔将它们全部记录在一份ISA参考手册中。他们改用了ARPL
32位模式下的1字节操作码,因此movsxd
实际上比movsx
8或16位源短1字节(假设您仍需要REX前缀才能扩展到64位)。
不同的目标大小使用具有不同操作数大小1的相同操作码。(66
或REX.W
16位或64位前缀,而不是默认的32位前缀。)例如,movsx eax, bl
并且movsx rax, bl
仅REX前缀有所不同;相同的操作码。(movsx ax, bl
也相同,但前缀为66以使操作数大小为16位。)
在AMD64之前,不需要读取32位源的操作码,因为最大目标宽度为32位,并且“符号扩展”到相同大小只是一个副本。注意这movsxd eax, eax
是合法的,但不建议这样做。您甚至可以使用66
前缀对其进行编码,以读取32位源并写入16位目标2。
不建议在64位模式下使用不带REX.W的MOVSXD。应该使用常规的MOV代替没有REX.W的MOVSXD。
可以使用32-> 64位符号扩展来cdq
将EAX符号扩展到EDX:EAX(例如32位之前idiv
)。这是x86-64之前的唯一方法(当然,除了复制和使用算术右移之外,还可以广播符号位)。
但是AMD64已经可以通过任何写入32位寄存器的指令将32从64扩展为64。 这样可以避免错误的依存关系,从而无序执行,这就是为什么AMD打破了8086/386的传统,即在写入部分寄存器时不影响高位字节的传统。(为什么GCC不使用部分寄存器?)
由于每个源宽度需要不同的操作码,因此没有前缀可以使两个movzx
操作码中的任何一个读取32位源。
有时您确实需要花费一条指令对某些内容进行零扩展。在小型函数的编译器输出中很常见,因为x86-64 SysV和Windows x64调用约定在args和返回值中允许大量垃圾。
像往常一样,询问编译器是否想知道如何在asm中执行某些操作,尤其是在没有看到所需指令的情况下。我ret
在每个函数的末尾都省略了。
来自Godbolt编译器浏览器的Source + asm,用于System V调用约定(RDI,RSI,RDX等中的参数):
#includeuint64_t zext(uint32_t a) { return a; } uint64_t extract_low(uint64_t a) { return a & 0xFFFFFFFF; } # both compile to mov eax, edi int use_as_index(int *p, unsigned a) { return p[a]; } # gcc mov esi, esi # missed optimization: mov same,same can't be eliminated on Intel mov eax, DWORD PTR [rdi+rsi*4] # clang mov eax, esi # with signed int a, we'd get movsxd mov eax, dword ptr [rdi + 4*rax] uint64_t zext_load(uint32_t *p) { return *p; } mov eax, DWORD PTR [rdi] uint64_t zext_add_result(unsigned a, unsigned b) { return a+b; } lea eax, [rdi+rsi]
在x86-64中,默认地址大小为64。高垃圾不会影响加法的低位,因此这节省了一个字节,lea eax, [edi+esi]
而字节需要67个地址大小的前缀,但每个输入都得到相同的结果。当然,add edi, esi
会在RDI中产生零扩展的结果。
uint64_t zext_mul_result(unsigned a, unsigned b) { return a*b; } # gcc8.1 mov eax, edi imul eax, esi # clang6.0 imul edi, esi mov rax, rdi # silly: mov eax,edi would save a byte here
英特尔建议mov
您选择时立即销毁结果,释放-消除所mov
占用的微体系结构资源,并提高- mov
消除的成功率(与AMD Ryzen不同,在Sandybridge系列中并非100%)。GCC对mov
/ 的选择imul
是最好的。
同样,在没有消除运动的CPU mov
上,如果其他输入尚未准备好,则before imul可能不在关键路径上(即,如果关键路径通过了未得到mov
ed 的输入)。但是mov
之后imul
取决于两个输入,因此始终处于关键路径上。
当然,当这些函数内联时,除非它们来自函数返回值,否则编译器通常将知道寄存器的完整状态。并且它也不需要在特定的寄存器中产生结果(RAX返回值)。但是,如果您的源代码unsigned
与size_t
或混合使用时过于草率uint64_t
,则可能会迫使编译器发出指令以截断64位值。(查看编译器的asm输出是捕获该错误并弄清楚如何调整源代码以使编译器保存指令的好方法。)
脚注1:有趣的事实:AT&T语法(使用movswl
(sign-extend word-> long(dword)或movzbl
)等不同的助记符)可以从寄存器中推断出目标大小,例如movzb %al, %ecx
,即使没有歧义也不会汇编movz %al, %ecx
。将movzb
其视为自己的助记符,并具有通常的操作数大小后缀(可以推断或显式),这意味着每个不同的操作码在AT&T语法中都有其自己的助记符。
有关EAX-> RAX的CDQE和任何寄存器的MOVSXD之间的冗余的历史课程,另请参见Assembly cltq和movslq差异。请参阅cltq在组装中做什么?或针对AT&T与Intel规范的GAS文档(零扩展/符号扩展)。
脚注2:愚蠢的计算机技巧movsxd ax, [rsi]
:
汇编程序拒绝汇编movsxd eax, eax
或movsxd ax, eax
,但是可以对其进行手动编码。 ndisasm
甚至都没有反汇编(只是db 0x63
),但是GNU objdump
确实可以反汇编。实际的CPU也会对其进行解码。我尝试在Skylake上只是为了确保:
; NASM source ; register value after stepi in GDB mov rdx, 0x8081828384858687 movsxd rax, edx ; RAX = 0xffffffff84858687 db 0x63, 0xc2 ;movsxd eax, edx ; RAX = 0x0000000084858687 xor eax,eax ; RAX = 0 db 0x66, 0x63, 0xc2 ;movsxd ax, edx ; RAX = 0x0000000000008687
那么,CPU如何在内部对其进行处理?它是否实际读取32位,然后截断为操作数大小? 事实证明,Intel的ISA参考手册将16位格式记录为63 /r
MOVSXD r16, r/m16
,所以movsxd ax, [unmapped_page - 2]
不会出错。 (但是它错误地记录了非REX格式在兼容/旧版模式下有效;实际上0x63
在此处解码为ARPL。这不是英特尔手册中的第一个错误。)
这非常有道理:硬件可以在简单地解码相同的UOP mov r16, r/m16
或mov r32, r/m32
当没有REX.W前缀。或不!
Skylake的movsxd eax,edx
(但不是movsxd rax, edx
)对目标寄存器具有输出依赖性,就像它正在合并到目标中一样! 一个循环,times 4
db 0x63, 0xc2 ; movsx eax, edx
每个迭代以4个时钟运行(每个循环1个movsxd
,因此1个周期延迟)。微指令相当均匀地分布到所有4个整数ALU执行端口。带有movsxd eax,edx
// movsxd ebx,edx
2个其他目标的循环在每次迭代中以〜1.4个时钟运行(如果使用普通4x mov eax, edx
或4x,则略小于每次迭代前端瓶颈1.25个时钟movsxd rax, edx
)。具有定时perf
在Linux上i7-6700k。
我们知道这样movsxd eax, edx
做会将RAX的高位清零,因此实际上并没有使用它正在等待的目标寄存器中的任何位,而是大概在内部对16位和32位进行了类似的处理,从而简化了解码,并且简化了这种极端情况下编码的处理,任何人都不应该这样做曾经使用过。16位格式始终必须实际合并到目标中,因此它确实对输出reg有真正的依赖性。(Skylake不会将全名寄存器单独重命名16位reg。)
GNU binutils错误地分解了它:gdb和objdump将源操作数显示为32位,例如
4000c8: 66 63 c2 movsxd ax,edx 4000cb: 66 63 06 movsxd ax,DWORD PTR [rsi]
什么时候应该
4000c8: 66 63 c2 movsxd ax,dx 4000cb: 66 63 06 movsxd ax,WORD PTR [rsi]
在AT&T语法中,objdump仍然有趣地使用movslq
。因此,我想它会将其视为一个整体助记符,而不是将其视为movsl
具有q
操作数大小的指令。或这仅仅是因为没人关心气体无论如何都不会聚集的特殊情况(它拒绝movsll
,并检查寄存器宽度是否为movslq
)。
在检查手册之前,我实际上在NASM上的Skylake上进行了测试,以查看负载是否会发生故障。它当然不会:
section .bss align 4096 resb 4096 unmapped_page: ; When built into a static executable, this page is followed by an unmapped page on my system, ; so I didn't have to do anything more complicated like call mmap ... _start: lea rsi, [unmapped_page-2] db 0x66, 0x63, 0x06 ;movsxd ax, [rsi]. Runs without faulting on Skylake! Hardware only does a 2-byte load o16 movsxd rax, dword [rsi] ; REX.W prefix takes precedence over o16 (0x66 prefix); this faults mov eax, [rsi] ; definitely faults if [rsi+2] isn't readable
请注意,这movsx al, ax
是不可能的:字节操作数大小需要单独的操作码。前缀仅在32(默认),16位(0x66)和长模式64位(REX.W)之间选择。 movs/zx ax, word [mem]
从386开始就可以使用,但是读取比目标更宽的源是x86-64中新增的特殊情况,并且仅用于符号扩展。(事实证明,16位目标编码实际上仅读取16位源。)
顺便说一句,AMD 可以(但不是)将AMD64设计为始终对32位寄存器写入进行符号扩展而不是始终为零扩展。在大多数情况下,这对于软件来说不太方便,并且可能还需要使用一些额外的晶体管,但是仍然可以避免错误地依赖寄存器中的旧值。这可能会在某处增加额外的门控延迟,因为结果的高位取决于低位,与零扩展不同,零扩展只取决于它是32位运算的事实。(但这可能并不重要。)
如果 AMD设计了这样的说法,他们就会需要一个movzxd
代替的movsxd
。我认为,将位域打包到更宽的寄存器中时,此设计的主要缺点是需要额外的说明。例如,在写入and 之后的shl rax,32
/ or rax, rdx
之后rdtsc
,免费零扩展名很方便。如果是符号扩展名,则需要一条指令将之前的高字节清零。edx
eax
rdx
or
其他ISA也做出了不同的选择:MIPS III(在1995年左右)将体系结构扩展到64位,而没有引入新的模式。与x86非常不同,在固定宽度的32位指令字格式中,有足够的操作码空间未使用。
MIPS最初是一种32位体系结构,从没有16位8086传统和32位x86完全支持32位x86的传统遗留部分寄存器内容,而AX = AH :AL部分规则等,可轻松移植8080源代码。
MIPS 32位算术指令(例如addu
在64位CPU上)要求其输入正确进行符号扩展,并产生符号扩展的输出。 (一切都只是工作运行传统的32位代码,不知道广大寄存器的时候,因为转移是特殊的。)
ADDU rd, rs, rt
(摘自MIPS III手册,第A-31页)限制:
在64位处理器上,如果GPR rt或GPR rs不包含符号扩展的32位值(位63..31相等),则操作的结果不确定。操作方式:
if (NotWordValue(GPR[rs]) or NotWordValue(GPR[rt])) then UndefinedResult() endif temp ?GPR[rs] + GPR[rt] GPR[rd]? sign_extend(temp31..0)
(请注意addu
,如手册所指出的,U表示未登录的,实际上是一个误称。除非您确实想add
陷在有符号的溢出上,否则也将其用于有符号的算术。)
有一个DADDU
针对双字ADDU的说明,它可以满足您的期望。类似地,DDIV / DMULT / DSUBU和DSLL等移位。
按位运算保持不变:现有的AND操作码变为64位AND;不需要64位AND,也不需要32位AND结果的免费符号扩展。
MIPS 32位移位是特殊的(SLL是32位移位。DSLL是单独的指令)。
SLL移位字左逻辑
操作方式:
s ? sa temp ? GPR[rt] (31-s)..0 || 0 s GPR[rd]? sign_extend(temp)编程说明:
与几乎所有其他字操作不同,输入操作数不必是正确的符号扩展字值即可产生有效的符号扩展32位结果。结果字始终被符号扩展到64位目标寄存器中。该移位量为零的指令将64位值截断为32位,并对其进行符号扩展。
我认为SPARC64和PowerPC64在保持窄结果的符号扩展方面类似于MIPS64。 代码生成用于(a & 0x80000000) +- 12315
对int a
(与-fwrapv
这样编译器不能假定a
非负的,因为签署溢出UB)的节目铿锵的PowerPC64维持或重拾符号扩展,和铛-target sparc64
安定然后或运算,以确保在低温只有合适的位设置32,再次保持符号扩展。将返回类型或arg类型更改为AND掩码常量long
或L
在AND掩码常量上添加后缀会导致MIPS64和PowerPC64,有时甚至是SPARC64的代码有所不同。也许只有MIPS64实际上会在输入没有正确符号扩展的32位指令上出错,而在其他情况下,这仅仅是软件调用约定要求。
但是AArch64采用的方法更类似于x86-64,w0..31
寄存器是的下半部分x0..31
,并且指令有两种操作数大小。
对于这些示例函数,我在上面的Godbolt链接中包括了MIPS64编译器输出。(还有一些其他的东西可以告诉我们更多有关调用约定以及什么是编译器的信息。)它通常需要dext
从32位零扩展到64位。但是直到mips64r2才添加该指令。使用-march=mips3
,return p[a]
因为无符号a
必须使用两个双字移位(左移然后右移32位)以零扩展!它还需要一条额外的指令来对添加结果进行零扩展,即实现从无符号到的转换uint64_t
。
因此,我认为我们很高兴x86-64设计为具有免费的零扩展名,而不是仅为某些事情提供64位操作数大小。(就像我说的那样,x86的传统非常不同;对于使用前缀的相同操作码,它已经具有可变的操作数大小。)当然,更好的位域指令会更好。其他一些ISA(例如ARM和PowerPC)使x86显得羞愧,无法有效地进行位域插入/提取。