assembly 使用操作数大小覆盖前缀0x66进行指令对齐

wribegjk  于 2023-01-02  发布在  其他
关注(0)|答案(1)|浏览(165)

最近我遇到了遗留的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
js5cn81o

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个条目,不需要额外的周期来读取它)。
dscs前缀是不错的选择,因为它们对大多数操作码都有意义,所以cs mov eax, 1不太可能被重新用作某些不同指令的编码。(例如rep bsrlzcnt的编码方式,它做了一些不同的事情。)这不是不可能的,特别是对于非modrm的mov-to-register操作码(mov r32,imm32mov 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%确定在纸上定义好了66hREX.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上,至少在正确性方面是安全的,如果不是性能的话。)

相关问题