最近我遇到了遗留的0x66操作数大小覆盖前缀。
它是否可以用于对齐指令,而无需显式写入单字节或多字节NOP
指令?
例如,添加align 16
指令:
int 3
mov rax,1
align 16
add rcx,rax
生成此反汇编:
...1000 cc int 3
...1001 48c7c001000000 mov rax,1
...1008 0f1f840000000000 nop dword ptr [rax+rax] ; <--- multi-byte NOP instruction
...1010 4803c8 add rcx,rax ; <--- 16-byte aligned
正在删除align 16
并在mov rax, 1
前面添加重复的0x66个忽略字节:
int 3
db 8 DUP (66h)
mov rax,1
add rcx,rax
生成此反汇编:
...1000 cc int 3
...1001 666666666666666648c7c001000000 mov rax,1
...1010 4803c8 add rcx,rax ; <--- 16-byte aligned
0x66对齐技术是否有效,是否比使用align 16
更快?
- 更新**
按照建议,它使用0x2E CS segment override前缀工作。
nop
CSbuf: times 8 db 2Eh
mov rax,strict dword 1
add rcx,rax
并且add rcx,rax
是16字节对齐的:
00007ff7`f78d1010 4801c1 add rcx,rax
使用以下命令生成:
nasm -fwin64 test.asm
link.exe /subsystem:console /machine:x64 /defaultlib:kernel32.lib /defaultlib:user32.lib /defaultlib:libcmt.lib /entry:main test.obj
1条答案
按热度按时间js5cn81o1#
基本思想是好的,使用前缀填充较早的指令是比使用一个多字节NOP更便宜的对齐方式。(NOP占用解码器、微指令缓存和发布/重命名阶段中的一个槽,这是流水线最窄的部分。还有一个ROB条目跟踪它直到退役)。
这就是英特尔建议在其针对JCC勘误表的微码更新引入working around the performance pothole时延长内循环中其它指令的原因。
汇编程序应该始终为
align
指令执行此操作(或者使用其他一些机制来指定要填充哪些指令),但是直到Intel的JCC勘误表正式推荐这样做之前,没有人有足够的动力。(因为与对齐循环顶部不同,填充可能必须在内部循环的 * 内部 *,如果它是NOP而不是其它指令的一部分,则每次迭代都将消耗前端带宽。)不幸的是,这种JCC错误缓解大多数是由汇编器作为单独的特性添加的(例如GAS的-mbranches-within-32B-boundaries
),或留给编译器,不提供通用的方法来避免在NOP上浪费指令来对齐其他点。您的具体选择并不理想
请参阅What methods can be used to efficiently extend instruction length on modern x86?-**一条指令上超过3个前缀可能会导致某些CPU的速度大幅下降,包括Silvermont系列,如桤木Lake中的E-core。**因此,请在基本块中您希望对齐的位置之前将此扩展到多条指令上。首选填充后面的指令,以便前端可以更早地解码更多指令。除非您有许多非常短的指令(如每个指令1或2字节),这些指令足够短,以至于16字节的获取块仍然可以包含5或6个以上的指令。(如果还没有,则预期将来的CPU将具有广泛的传统解码功能。)
在汇编器中使用7字节
mov rax, 1
,而不是将其优化为5字节mov eax, 1
,这已经是一种填充某些字节的方法; 10字节mov rax, strict qword 1
(NASM语法; IDK,如果MASM可以强制imm 64)是使用更多字节的另一种方法。在Sandybridge系列中,当64位值不是很大时,即只是32位值的符号扩展时,64位立即数可以有效地放入uop缓存(1个条目,不需要额外的周期来读取它)。ds
或cs
前缀是不错的选择,因为它们对大多数操作码都有意义,所以cs mov eax, 1
不太可能被重新用作某些不同指令的编码。(例如rep bsr
是lzcnt
的编码方式,它做了一些不同的事情。)这不是不可能的,特别是对于非modrm的mov-to-register操作码(mov r32,imm32
或mov r64,imm64
,与您使用的mov r/m64, sign_extended_imm32
不同。https://www.felixcloutier.com/x86/mov)我不建议使用
66h
这样的前缀,因为它可能导致Intel CPU上出现LCP pre-decode stalls,甚至改变许多指令的含义。(如果不将雷克斯.W的操作数大小设置为64位,则会将mov eax, 0x00000001
的含义更改为mov ax, 0x0001
,并留下00 00
,解码为add [rax], al
。)我不能100%确定在纸上定义好了
66h
和REX.W
前缀应该发生什么。(@fuz在评论中对此提出了质疑)67h
地址大小前缀在64位模式下通常是好的,或者段覆盖前缀在64位模式下也是好的。在Skylake上的实践中,雷克斯.W前缀获胜,66 h被忽略,甚至不会导致错误的LCP停顿。但我不会指望P6系列上会出现这种情况,如果没有书面记录66 h和REX.W会发生什么,我会担心其他供应商,或者特别是模拟器和动态转换软件。
有趣的事实:Sandybridge系列通常不会在
mov
上停止LCP,但当66h
前缀将操作码从imm32
更改为imm16
时,会在其他指令上停止。我刚刚试过这个,用NASM
times 6 db 0x66
/mov rax, strict dword 1
(来匹配你的编码; NASM通常将其优化为架构上等效的mov eax,1
)。我将其放在%rep 8000
块中(以击败uop缓存)。它在Skylake上以1.2IPC运行,在perf stat
中没有ild_stall.lcp
的计数。即使使用
add rax, strict qword 1
(强制add rax, sign_extended_imm32
非modrm编码),Skylake也不会停止LCP,以1.0 IPC运行(延迟瓶颈)。我不建议使用66 h前缀,但它不会破坏Skylake上的正确性(使用雷克斯.W)或性能。我没有在任何其他CPU或模拟器上进行测试,我也没有声称
66h
的这种使用在其他任何地方都是安全的。(尽管它可能在早期的Intel CPU上,至少在正确性方面是安全的,如果不是性能的话。)