添加这些 NOP 的不是我,而是一个汇编程序。它非常愚蠢并且不支持对齐选项(BASM) - 只有一个选项 - 边界大小。
我不知道“BASM”是什么,我在网上找不到任何对它的引用(除了this,它显然不是 x86),但如果它不支持多字节 NOP,你真的需要一个不同的汇编程序。这只是多年来一直在英特尔和 AMD 架构手册中的基本内容。Gnu 汇编器可以为 ALIGN 指令执行此操作,Microsoft 的 MASM 也可以。开源的NASM和YASM汇编器也支持这一点,并且它们中的任何一个都可以轻松地集成到任何现有的构建系统中。
多字节 NOP 是指以下内容,您可以在 AMD 和 Intel 处理器手册中找到:
Length | Mnemonic | Opcode Bytes
---------|-------------------------------------------|-------------------------------------
1 byte | NOP | 90
2 bytes | 66 NOP | 66 90
3 bytes | NOP DWORD [EAX] | 0F 1F 00
4 bytes | NOP DWORD [EAX + 00H] | 0F 1F 40 00
5 bytes | NOP DWORD [EAX + EAX*1 + 00H] | 0F 1F 44 00 00
6 bytes | 66 NOP DWORD [EAX + EAX*1 + 00H] | 66 0F 1F 44 00 00
7 bytes | NOP DWORD [EAX + 00000000H] | 0F 1F 80 00 00 00 00
8 bytes | NOP DWORD [EAX + EAX*1 + 00000000H] | 0F 1F 84 00 00 00 00 00
9 bytes | 66 NOP DWORD [EAX + EAX*1 + 00000000H] | 66 0F 1F 84 00 00 00 00 00
两家制造商提供的序列建议在 9 个字节后略有不同,但这么长的 NOP ……并不是很常见。并且可能并不重要,因为带有过多前缀的极长 NOP 指令无论如何都会降低性能。这些一直可以追溯到 Pentium Pro,因此它们在今天得到了普遍支持。
Agner Fog 对多字节 NOP 有这样的说法:
多字节 NOP 指令具有操作码0F 1F
+ 一个虚拟内存操作数。多字节 NOP 指令的长度可以通过在虚拟内存操作数中添加 1 个或 4 个字节的位移和一个 SIB 字节以及添加一个或多个66H
前缀来调整。过多的前缀可能会导致较旧的微处理器出现延迟,但在大多数处理器上至少有两个前缀是可以接受的。以这种方式可以构造不超过 10 个字节的任何长度的 NOP,前缀不超过两个。如果处理器可以处理多个前缀而不会受到惩罚,那么长度可以达到 15 个字节。
所有多余的/多余的前缀都被简单地忽略了。当然,优势在于许多较新的处理器对多字节 NOP 的解码率较低,因此效率更高。它们将比一系列 1 字节 NOP ( 0x90
) 指令更快。
也许比多字节 NOP 更好的对齐方式是使用您已经在代码中使用的较长形式的指令。这些较长的编码不再需要执行(它们只影响解码带宽),因此它们比 NOP 更快/更便宜。这方面的例子是:
- 使用 mod-reg-r/m 字节形式的指令,如
INC
, DEC
, PUSH
,POP
等,而不是短版本
- 使用更长的等效指令,例如
ADD
代替INC
或LEA
代替MOV
.
- 编码较长形式的立即数操作数(例如,32 位立即数而不是符号扩展的 8 位立即数)
- 添加 SIB 字节和/或不必要的前缀(例如,操作数大小、段和长模式下的 REX)
Agner Fog 的手册也详细介绍了这些技术并给出了示例。
我不知道有任何汇编程序会自动为您进行这些转换/优化(出于显而易见的原因,汇编程序会选择最短的版本),但它们通常具有严格的模式,您可以在其中强制使用特定的编码,或者您可以手动发出指令字节。无论如何,您只能在对性能高度敏感的代码中执行此操作,而工作实际上会得到回报,因此大大限制了所需工作的范围。
我想知道添加这些 NOP 的额外代码大小和额外执行时间是否物有所值。
一般来说,没有。虽然数据对齐非常重要并且本质上是免费的(尽管二进制文件的大小),但代码对齐的重要性要小得多。在紧密循环中的某些情况下,它可以产生显着的差异,但这仅在您的代码中的热点中很重要,您的分析器已经识别了这些热点,然后您可以执行操作以在必要时手动对齐代码。否则,我不会担心。
对齐函数是有意义的,因为它们之间的填充字节永远不会执行(而不是在这里使用 NOP,您会经常看到无效INT 3
指令,例如UD2
),但我不会四处对齐所有分支目标理所当然地发挥作用。仅在已知的关键内部循环中执行此操作。
与以往一样,Agner Fog 谈到了这一点,并且比我说得更好:
大多数微处理器以对齐的 16 字节或 32 字节块获取代码。如果一个重要的子程序入口或跳转标签碰巧接近一个 16 字节块的末尾,那么微处理器在获取该代码块时只会得到几个有用的代码字节。在解码标签后的第一条指令之前,它可能也必须获取接下来的 16 个字节。这可以通过将重要的子程序入口和循环入口对齐 16 来避免。对齐 8 将确保在第一次取指时可以加载至少 8 个字节的代码,如果指令很小,这可能就足够了。如果子例程是关键热点的一部分并且前面的代码不太可能在相同的上下文中执行,我们可以按缓存行大小(通常为 64 字节)对齐子例程条目。
代码对齐的一个缺点是,在对齐的代码条目之前,一些缓存空间会丢失到空白空间。
在大多数情况下,代码对齐的影响很小。所以我的建议是只在最关键的情况下对齐代码,比如关键的子程序和关键的最内层循环。
对齐子程序入口很简单,只需在子程序入口前放置NOP
所需数量的 ',以便根据需要使地址可被 8、16、32 或 64 整除。汇编器使用ALIGN
指令执行此操作。插入的NOP
's 不会降低性能,因为它们永远不会被执行。
对齐循环条目的问题更大,因为前面的代码也被执行了。最多可能需要 15NOP
来将循环条目对齐 16。这些NOP
' 将在进入循环之前执行,这将花费处理器时间。使用更长的指令比使用大量的单字节指令更有效NOP
。最好的现代汇编程序会做到这一点,并使用诸如MOV EAX,EAX
和
之类的指令来填充语句LEA EBX,[EBX+00000000H]
之前的空间。指令特别灵活ALIGN nn
。LEA
可以给出类似的指令LEA EBX,[EBX]
通过不同地添加一个 SIB 字节、一个段前缀和一个或四个零字节的偏移量,可以得到从 2 到 8 的任意长度。不要在 32 位模式下使用两字节偏移,因为这会减慢解码速度。并且不要使用多个前缀,因为这会减慢旧英特尔处理器的解码速度。
使用伪 NOP(例如MOV RAX,RAX
和LEA RBX,[RBX+0]
作为填充符)的缺点是它对寄存器有错误的依赖性,并且会使用执行资源。最好使用多字节 NOP 指令,可以调整到所需的长度。多字节 NOP 指令适用于所有支持条件移动指令的处理器,即 Intel PPro、P2、AMD Athlon、K7 及更高版本。
对齐循环条目的另一种方法是将前面的指令编码为比必要更长的方式。在大多数情况下,这不会增加执行时间,但可能会增加取指时间。
他还继续展示了通过移动前面的子例程条目来对齐内部循环的另一种方法的示例。这有点尴尬,即使在最好的汇编程序中也需要进行一些手动调整,但它可能是最优化的机制。同样,这仅在热路径上的关键内部循环中很重要,无论如何您可能已经在其中进行了挖掘和微优化。
有趣的是,我已经对正在优化的代码进行了多次基准测试,但并没有发现对齐循环分支目标有什么好处。例如,我正在编写一个优化strlen
函数(Gnu 库有,但微软没有),并尝试将主内循环的目标对齐在 8 字节、16 字节和 32 字节边界上。这些都没有太大的不同,尤其是与我在重写代码时所取得的其他重大性能进步相比时。
请注意,如果您不针对特定处理器进行优化,您可能会疯狂地试图找到最佳的“通用”代码。当谈到对齐对速度的影响时,情况可能会有很大的不同。糟糕的对齐策略通常比根本没有对齐策略更糟糕。
二次幂边界总是一个好主意,但这很容易实现,无需任何额外的努力。再说一次,不要忽视对齐,因为它可能很重要,但出于同样的原因,不要执着于尝试对齐每个分支目标。
在最初的 Core 2(Penryn 和 Nehalem)微架构上,对齐曾经是一个更大的问题,其中严重的解码瓶颈意味着,尽管问题宽度为 4,但您很难保持其执行单元忙碌。随着在 Sandy Bridge 中引入 µop 缓存(Pentium 4 中为数不多的好功能之一,最终被重新引入 P6 扩展系列),前端吞吐量显着增加,这变得不那么简单了。问题。
坦率地说,编译器也不太擅长进行这些类型的优化。GCC的-O2
开关意味着-falign-functions
、-falign-jumps
、-falign-loops
和-falign-labels
开关,默认首选项是在 8 字节边界上对齐。这是一种非常直率的方法,而且里程数各不相同。正如我在上面链接的那样,关于禁用这种对齐并使用紧凑代码是否实际上可以提高性能的报告各不相同。此外,您将看到编译器所做的最好的事情就是插入多字节 NOP。我还没有看到使用较长形式的指令或为了对齐目的而大幅重新排列代码的方法。所以我们还有很长的路要走,这是一个非常难解决的问题。有些人正在为此努力, 但这只是说明了这个问题实际上是多么棘手:“指令流中的小改动,例如插入一条 NOP 指令,可能会导致显着的性能差异,其效果是使编译器和性能优化工作暴露在感知到不想要的随机性。” (请注意,虽然很有趣,但该论文来自早期的 Core 2 天,正如我之前提到的,它遭受的错位惩罚比大多数人都多。我不确定你是否会在今天的微架构上看到同样的巨大改进,但是我不能肯定,因为我还没有进行测试。也许谷歌会雇用我,我可以发表另一篇论文?)
Skylake 在一个周期内可以执行多少个 1 字节的 NOP?其他与 Intel 兼容的处理器(如 AMD)呢?我不仅对 Skylake 感兴趣,还对其他微架构感兴趣。执行 15 个 NOP 的序列可能需要多少个周期?
此类问题可以通过查看 Agner Fog 的说明表并搜索NOP
. 我不会费心将他的所有数据提取到这个答案中。
不过,一般来说,只要知道 NOP 不是免费的。尽管它们不需要执行单元/端口,但它们仍然必须像任何其他指令一样通过管道运行,因此它们最终会受到处理器问题(和/或退役)宽度的限制。这通常意味着您可以在每个时钟执行 3 到 5 个 NOP。
NOP 仍然会占用 µop 缓存中的空间,这意味着代码密度和缓存效率会降低。
在许多方面,您可以将 aNOP
视为等同于 aXOR reg, reg
或MOV
由于寄存器重命名而在前端被省略。