想象一下,您希望将一系列x86汇编指令与某些边界对齐 . 例如,您可能希望将循环对齐到16或32字节的边界,或者将指令打包以使它们有效地放置在uop缓存中或其他任何位置 .
实现这一目标的最简单方法是单字节NOP指令,紧接着是multi-byte NOPs . 虽然后者通常效率更高,但这两种方法都不是免费的:NOP使用前端执行资源,并且在现代x86上也计入4宽1重命名限制 .
另一个选择是以某种方式延长一些指令以获得所需的对齐 . 如果这样做没有引入新的停顿,它似乎比NOP方法更好 . 如何在最近的x86 CPU上有效地延长指令?
在理想的世界中,延长技术同时是:
-
适用于大多数说明
-
能够以可变数量延长指令
-
不会停止或以其他方式减慢解码器的速度
-
在uop缓存中有效表示
有一种方法不可能同时满足所有上述要点,因此很好的答案可能会解决各种权衡问题 .
1 AMD Ryzen的限制为5或6 .
4 回答
Consider mild code-golfing to shrink your code instead of expanding it ,特别是在循环之前 . 例如
xor eax,eax
/cdq
如果需要两个归零寄存器,或者mov eax, 1
/lea ecx, [rax+1]
将寄存器设置为1和2,总共只有8个字节而不是10个 . 有关更多信息,请参阅Set all bits in CPU register to 1 efficiently,有关更多一般概念,请参阅Tips for golfing in x86/x64 machine code . 不过,可能你仍然希望避免错误的依赖 .Or fill extra space by creating a vector constant on the fly 而不是从内存中加载它 . (但是,对于包含设置内部循环的较大循环,添加更多uop-cache压力可能会更糟 . 但它避免了常量的d-cache错误,因此它具有补偿运行更多uops的优势 . )
如果您尚未使用它们加载"compressed"常量,
pmovsxbd
,movddup
或vpbroadcastd
的长度超过movaps
. dword / qword广播加载是免费的(没有ALU uop,只是一个加载) .如果你可能会担心它在L1I缓存中的位置或uop-cache边界的位置,那么只计算总uops就不够了,在你关心的那个之前块中的一些额外的uops可能不是一个问题 .
但在某些情况下,您可能真的希望在要对齐的块之前优化指令的解码吞吐量/ uop-cache使用/总uop .
填充说明,如要求的问题:
Agner Fog has a whole section on this: "10.6 Making instructions longer for the sake of alignment" 在他的"Optimizing subroutines in assembly language" guide . (
lea
,push r/m64
和SIB的想法来自那里,我复制了一两句话/句子,否则这个答案是我自己的作品,要么是不同的想法,要么是在检查Agner指南之前写的 . )但是,它尚未针对当前CPU进行更新:
lea eax, [rbx + dword 0]
具有比用于vs2905457_更多的缺点,因为您错过了zero-latency / no execution unit mov . 如果它不在关键路径上,那就去吧 . 简单的lea
具有相当好的吞吐量,具有大寻址模式(甚至可能是一些段前缀)的LEA可以比mov
nop
更好地解码/执行吞吐量 .使用通用表单而不是简短形式(无ModR / M)的指令,如
push reg
或mov reg,imm
. 例如使用2字节push r/m64
表示push rbx
. 或者使用更长的等效指令,例如add dst, 1
而不是inc dst
,in cases where there are no perf downsides to inc,因此您已经在使用inc
.Use SIB byte . 您可以通过使用单个寄存器作为索引来使NASM执行此操作,例如
mov eax, [nosplit rbx*1]
(see also),但这会损害负载使用延迟,而不是简单地使用SIB字节编码mov eax, [rbx]
. 索引寻址模式在SnB系列上有其他缺点,like un-lamination and not using port7 for stores .所以 it's best to just encode base=rbx + disp0/8/32=0 using ModR/M + SIB with no index reg . ("no index"的SIB编码是否则意味着idx = RSP的编码) .
[rsp + x]
寻址模式已经需要一个SIB(base = RSP是转义代码,这意味着现在和将来都有很好的理由期望它能够完全有效地解码和执行(即使对于RSP以外的基址寄存器) . 语法可以't express this, so you' d必须手动编码 . 来自objdump -d
的GNU gas Intel语法对于Agner Fog的例子10.20说8b 04 23 mov eax,DWORD PTR [rbx+riz*1]
. (riz
是虚构的索引 - 零符号,这意味着如果GAS接受它作为输入,则测试's a SIB with no index). I haven' .Use an imm32 and/or disp32 form of an instruction that only needed imm8 or disp0/disp32. Agner Fog 's testing of Sandybridge' s uop cache(microarch guide table 9.1)表示立即/位移的实际值是重要的,而不是指令编码中使用的字节数 . 我没有't have any info on Ryzen'的uop缓存 .
所以NASM
imul eax, [dword 4 + rdi], strict dword 13
(10字节:操作码modrm disp32 imm32)将使用32small,32small类别并在uop缓存中取1个条目,这与immediate或disp32实际上有超过16个有效位不同 . (然后它将需要2个条目,并从uop缓存加载它将需要一个额外的周期 . )根据Agner 's table, 8/16/32small are always equivalent for SnB. And addressing modes with a register are the same whether there' s根本没有位移,或者它是否为32small,所以
mov dword [dword 0 + rdi], 123456
需要2个条目,就像mov dword [rdi], 123456789
一样 . 我没有意识到[rdi]
完整的imm32拿了2个条目,但显然是'在SnB上就是这种情况 .Use jmp / jcc rel32 instead of rel8 . 理想情况下,尝试在不会扩展的地方扩展说明 . Pad after jump targets for earlier forward jumps, pad before jump targets for later backward jumps, 如果他们接近需要在其他地方使用rel32 . 即尽量避免分支与其目标之间的填充,除非您希望该分支仍然使用rel32 .
您可能想要将
mov eax, [symbol]
编码为64位代码中的6字节a32 mov eax, [abs symbol]
,使用地址大小前缀来使用32位绝对地址 . 但this does cause a Length-Changing-Prefix stall何时在Intel CPU上进行解码 . 幸运的是,如果没有明确指定32位地址大小,默认情况下NASM / YASM / gas / clang都不会执行此代码大小优化,而是使用带有ModR / M SIB disp32绝对寻址模式的7字节mov r32, r/m32
为mov eax, [abs symbol]
.In 64-bit position-dependent code, absolute addressing is a cheap way to use 1 extra byte vs. RIP-relative . 但请注意,32位绝对立即需要2个周期才能从uop缓存中获取,这与RIP相关的imm8 / 16/32不同,即使它仍然使用2个条目用于指令,它只需要1个周期 . (例如,对于
mov
-store或cmp
) . 所以cmp [abs symbol], 123
从uop缓存中获取的速度比cmp [rel symbol], 123
慢,即使每个都需要2个条目 . 没有立即,没有额外的成本请注意,PIE可执行文件甚至允许ASLR用于可执行文件and are the default in many Linux distro,因此如果您可以保持代码PIC没有任何缺陷,那么这是更可取的 .
Use a REX prefix when you don't need one, e.g. db 0x40 / add eax, ecx.
It's not in general safe to add prefixes like rep that current CPUs ignore, because they might mean something else in future ISA extensions.
有时可能会重复相同的前缀(但不能使用REX) . 例如,
db 0x66, 0x66
/add ax, bx
给出指令3个操作数大小的前缀,我认为它总是严格等同于前缀的一个副本 . 最多3个前缀是某些CPU上有效解码的限制 . 但这只适用于你有一个前缀,你可以在第一时间使用;你通常不想要32位地址大小(尽管在位置相关的代码中访问静态数据是安全的) .A ds or ss prefix on an instruction that accesses memory is a no-op ,并且可能不会导致任何当前CPU的任何减速 . (@prl在评论中建议这一点) .
事实上, Agner Fog's microarch guide uses a ds prefix on a movq [esi+ecx],mm0 in Example 7.1. Arranging IFETCH blocks 调整PII / PIII的循环(没有循环缓冲区或uop缓存),从每个时钟的3次迭代加速到2 .
当指令有超过3个前缀时,某些CPU(如AMD)会慢慢解码 . 在某些CPU上,这包括SSE2中的强制性前缀,尤其是SSSE3 / SSE4.1指令 . 在Silvermont中,即使0F转义字节也很重要 .
AVX instructions can use a 2 or 3-byte VEX prefix . 某些指令需要3字节的VEX前缀(第二个源是x / ymm8-15,或SSSE3或更高版本的必需前缀) . 但是,可以使用2字节前缀的指令始终可以使用3字节VEX进行编码 . NASM或GAS
{vex3} vxorps xmm0,xmm0
. 如果AVX512可用,您也可以使用4字节EVEX .Use 64-bit operand-size for mov even when you don't need it ,例如
mov rax, strict dword 1
强制NASM中的7字节sign-extended-imm32编码,which would normally optimize it to 5-byte mov eax, 1 .您甚至可以使用
mov reg, 0
而不是xor reg,reg
.mov r64, imm64 fits efficiently in the uop cache when the constant is actually small (fits in 32-bit sign extended.) 1 uop-cache条目,加载时间= 1,与
mov r32, imm32
相同 . 解码一个巨大的指令意味着's probably not room in a 16-byte decode block for 3 other instructions to decode in the same cycle, unless they'全部是2字节 . 稍微延长多个其他指令可能比使用一个长指令更好 .对额外前缀的解码处罚:
P5:前缀阻止配对,但PMMX上的地址/操作数大小除外 .
PPro到PIII:如果一条指令有多个前缀,总会有一个惩罚 . 这个惩罚通常是每个额外前缀一个时钟 . (Agner的微型指南,6.3节末)
Silvermont:如果您关心它,它可能是您可以使用哪些前缀的最严格约束 . 解码超过3个前缀,计算强制性前缀0F转义字节 . SSSE3和SSE4指令已经有3个前缀,因此即使是REX也会使解码速度变慢 .
某些AMD:可能是3前缀限制,不包括转义字节,也可能不包括SSE的强制性前缀说明 .
... TODO:完成本节 . 在此之前,请咨询Agner Fog的微型指南 .
After hand-encoding stuff, always disassemble your binary to make sure you got it right . 它更好地支持在指令区域上选择便宜的填充以达到给定的对齐边界 .
汇编语法
NASM has some encoding override syntax :
{vex3}
和{evex}
前缀,NOSPLIT
和strict byte / dword
,并强制在寻址模式中使用disp8 / disp32 . 请注意,不允许使用[rdi + byte 0]
,必须先使用byte
关键字 . 允许[byte rdi + 0]
,但我认为这看起来很奇怪 .列出
nasm -l/dev/stdout -felf64 padding.asm
GAS has encoding-override pseudo-prefixes , , , and These replace the now-deprecated .s, .d8 and .d32 suffixes .
GAS没有覆盖直接大小,只有位移 .
GAS确实允许你添加一个明确的
ds
前缀,ds mov src,dst
gcc -g -c padding.S && objdump -drwC padding.o -S
,手工编辑:GAS严格来说不像NASM那样表达超过需要的编码 .
我可以想到四个方面:
First: 使用备用编码作为指令(Peter Cordes提到类似的东西) . 例如,有很多方法可以调用ADD操作,其中一些方法占用更多字节:
http://www.felixcloutier.com/x86/ADD.html
通常,汇编程序会尝试为这种情况选择“最佳”编码,无论是针对速度还是长度进行优化,但是您总是可以使用另一个编码并获得相同的结果 .
Second: 使用其他含义相同但长度不同的说明 . 我相信你可以想到无数的例子,你可以将一条指令放入代码中来替换现有的指令并得到相同的结果 . 手动优化代码的人会一直这样做:
Third: 使用各种NOP来填补额外空间:
在理想的世界中,您可能必须使用所有这些技巧来使代码成为您想要的确切字节长度 .
Fourth: 使用上述方法更改算法以获得更多选项 .
最后一点:由于指令的数量和复杂性,显然针对更现代的处理器将为您提供更好的结果 . 访问MMX,XMM,SSE,SSE2,浮点等指令可以使您的工作更轻松 .
我们来看一段特定的代码:
对于此代码,没有任何指令可以替换为其他任何指令,因此唯一的选项是冗余前缀和NOP .
However, what if you change the instruction ordering?
您可以将代码转换为:
重新订购说明后;
mov al,0xFF
可以替换为or eax,0x000000FF
或or ax,0x00FF
.对于第一个指令排序,只有一种可能性,对于第二个指令排序,有3种可能性;因此,在不使用任何冗余前缀或NOP的情况下,总共有4种可能的排列可供选择 .
对于这4种排列中的每一种,您可以添加具有不同冗余前缀量的变体,以及单字节和多字节NOP,以使其以特定对齐结束 . 我懒得做数学,所以让我们假设它可能扩展到100种可能的排列 .
如果你给这100个排列中的每一个都得分(基于诸如执行需要多长时间,如果大小或速度很重要的话,如果大小或速度很重要,它与指令对齐的程度等等),该怎么办?这可以包括微架构定位(例如,对于某些CPU,原始置换会破坏微操作融合并使代码更糟) .
您可以生成所有可能的排列并给它们一个分数,并选择具有最佳分数的排列 . 请注意,这可能不是具有最佳对齐的排列(如果对齐不如其他因素重要,只会使性能变差) .
当然,您可以将大型程序分成许多由控制流变化分隔的小型线性指令组;然后对每组小线性指令进行“穷举搜索最佳得分排列” .
The problem is that instruction order and instruction selection are co-dependent.
对于上面的示例,在我们重新订购指令之前,您无法替换
mov al,0xFF
;然后重新订购说明,直到你完全关心性能为止 .取决于代码的性质 .
Floatingpoint重码
AVX prefix
对于大多数SSE指令,可以使用更长的AVX前缀 . 请注意,在intel CPU上切换SSE和AVX时存在固定的损失[1] [2] . 这需要vzeroupper,它可以被解释为SSE代码的另一个NOP或不需要更高的AVX代码128位 .
SSE/AVX NOPS
我能想到的典型NOP是:
XORPS相同的寄存器,对这些的整数使用SSE / AVX变体
ANDPS相同的寄存器,对这些的整数使用SSE / AVX变体