Max*_*tin 6 optimization x86 assembly alignment nop
我将分支目标与NOP对齐,有时CPU执行这些NOP,最多15个NOP.Skylake可以在一个周期内执行多少个1字节NOP?其他与AMD兼容的处理器如何?我不仅对Skylake感兴趣,而且对其他微架构也感兴趣.执行一系列15个NOP可能需要多少个周期?我想知道增加这些NOP的额外代码大小和额外执行时间是否物有所值.这不是我添加这些NOP而是每当我编写align
指令时自动添加汇编程序的人.
更新:我已经设法自动插入多字节NOP
s.
添加这些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
Run Code Online (Sandbox Code Playgroud)
两家制造商提供的顺序建议在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更快/更便宜。例如:
INC
,DEC
,PUSH
,POP
,等代替的,短的版本ADD
代替INC
或LEA
代替MOV
。Agner Fog的手册详细介绍了这些技术,并给出了示例。
我不知道有任何汇编程序会自动为您执行这些转换/优化(出于显而易见的原因,汇编程序会选择最短的版本),但是它们通常具有严格的模式,您可以在其中强制使用特定的编码,或者只能手动发出指令字节。无论如何,您都只能在对性能非常敏感的代码中执行此操作,因为工作实际上会取得回报,因此大大限制了所需工作的范围。
我想知道添加这些NOP的额外代码大小和额外执行时间是否值得。
一般来说,没有。尽管数据对齐非常重要并且基本上是免费的(尽管二进制文件大小),但是代码对齐的重要性却要低得多。在某些情况下,紧密循环可能会产生很大的不同,但这仅在代码的热点中才有意义,而探查器将已经识别出这些热点,然后可以根据需要执行操作以手动对齐代码。否则,我不会担心。
对齐函数很有意义,因为它们之间的填充字节永远不会执行(而不是在这里使用NOP,您会经常看到INT 3
或使用无效指令,例如UD2
),但是我不会在所有对齐分支目标的情况下进行对齐功能只是理所当然的事情。仅在已知的关键内部循环中执行此操作。
与以往一样,Agner Fog谈论了这一点,并说得比我能做到的更好:
大多数微处理器以对齐的16字节或32字节块来获取代码。如果重要的子例程条目或跳转标签恰好在16字节块的末尾附近,则微处理器在获取该代码块时将仅获得一些有用的代码字节。它可能还必须获取下一个16个字节,才能解码标签后的第一条指令。可以通过将重要的子例程条目和循环条目按16对齐来避免这种情况。按8对齐将确保至少有8个字节的代码可以装入第一条指令,如果指令很小,这可能就足够了。如果子例程是关键热点的一部分,并且前面的代码不太可能在相同的上下文中执行,则我们可以按高速缓存行大小(通常为64个字节)对齐子例程条目。
代码对齐的缺点是对齐的代码条目之前,某些缓存空间丢失为空白空间。
在大多数情况下,代码对齐的影响很小。因此,我的建议是仅在最关键的情况下(例如关键子例程和关键内部循环)对齐代码。
对齐子例程条目非常简单,
NOP
只需在子例程条目之前放置所需数量的,以根据需要使地址可被8、16、32或64整除。汇编器使用ALIGN
指令执行此操作。该NOP
被插入的,因为他们永远不会执行不会对性能减慢。对齐循环条目更成问题,因为前面的代码也已执行。
NOP
将一个循环条目乘以16 可能需要最多15个。这些NOP
将在进入循环之前执行,这将花费处理器时间。使用更长的无用指令比使用大量单字节指令更有效NOP
。最好的现代汇编程序将做到这一点,并在语句之前使用诸如MOV EAX,EAX
和LEA EBX,[EBX+00000000H]
填充指令ALIGN nn
。该LEA
说明特别灵活。可以给出类似的指令LEA EBX,[EBX]
通过添加一个SIB字节,一个段前缀以及一个或四个零字节的偏移量,可以从2到8的任何长度。不要在32位模式下使用两字节偏移量,因为这会减慢解码速度。并且不要使用多个前缀,因为这会减慢旧版Intel处理器上的解码速度。使用伪NOP(例如
MOV RAX,RAX
和LEA RBX,[RBX+0]
作为填充符)具有以下缺点:对寄存器有错误的依赖关系,并且使用执行资源。最好使用可以调整为所需长度的多字节NOP指令。多字节NOP指令在所有支持条件移动指令的处理器中都可用,例如Intel PPro,P2,AMD Athlon,K7和更高版本。对齐循环条目的另一种方法是以比必要的更长的方式对前面的指令进行编码。在大多数情况下,这不会增加执行时间,但可能会增加指令获取时间。
他还继续展示了通过移动前面的子例程条目来对齐内部循环的另一种方法的示例。这有点尴尬,即使是最好的汇编程序,也需要一些手动调整,但这可能是最佳的机制。同样,这仅在热路径上的关键内部循环中起作用,无论如何,您可能已经在其中进行了挖掘和微优化。
有趣的是,我已经基准测试了几次优化代码,并且没有发现对齐循环分支目标有什么好处。例如,我正在编写一个优化strlen
函数(Gnu库有一个,而Microsoft没有),并尝试将主内部循环的目标对准8字节,16字节和32字节的边界。这些都没有太大的不同,尤其是与我在重写代码时取得的其他出色性能相比时,并没有什么不同。
并且要注意,如果您没有针对特定的处理器进行优化,则可能会因为试图找到最佳的“通用”代码而使自己发疯。当谈到对齐对速度的影响时,事情可能千差万别。不良的对齐策略通常比根本没有对齐策略更糟糕。
2的幂边界始终是一个好主意,但这很容易实现,无需任何额外的努力。再次,不排除对准不可收拾,因为它可以不管,但出于同样的原因,不要迷恋关于设法对准每一个分支目标。
对齐在以前的原始Core 2(Penryn和Nehalem)微体系结构上要大得多,在该体系中,大量的解码瓶颈意味着,尽管问题宽度为4宽,但您仍然很难保持其执行单元繁忙。随着在Sandy Bridge中引入µop缓存(Pentium 4的几个不错的功能之一,最终又将其重新引入P6扩展系列中),前端吞吐量显着提高了,而这已经不像以前那么多了。问题。
坦白说,编译器也不擅长进行此类优化。所述-O2
用于GCC开关意味着-falign-functions
,-falign-jumps
,-falign-loops
,和-falign-labels
开关,具有缺省优先级为对准在8字节边界。这是一种很钝的方法,而且里程数也有所不同。正如我上面所链接的,关于禁用此对齐方式并使用紧凑代码是否可能实际上会提高性能的报告有所不同。此外,最好的情况是,您将看到编译器正在执行的操作是插入多字节NOP。我还没有看到使用更长形式的指令或为了对齐目的而大幅度重新排列代码的人。因此,我们仍然有很长的路要走,这是一个非常难以解决的问题。有些人正在努力,但这只是说明问题的实质是多么棘手:“指令流中的小变化,例如插入单个NOP指令,可能会导致明显的性能差异,从而使编译器和性能优化工作暴露于感觉到不必要的随机性。” (请注意,虽然有趣,但是这篇论文是在Core 2早期发布的,正如我之前提到的,它遭受的对齐偏移损失最多,我不知道您是否会在当今的微体系结构上看到同样的大幅改进,但是我不能肯定地说哪种方式,因为我还没有运行测试。也许Google会雇用我,我可以发表另一篇论文?)
Skylake在一个周期内可以执行多少个1字节的NOP?那么其他兼容Intel的处理器(例如AMD)呢?我不仅对Skylake感兴趣,而且对其他微型体系结构也很感兴趣。执行15个NOP序列需要多少个周期?
可以通过查看Agner Fog的说明表并搜索来回答此类问题NOP
。我不会费心将他的所有数据提取到此答案中。
但是,总的来说,只要知道NOP不是免费的。尽管它们不需要执行单元/端口,但它们仍必须像其他任何指令一样在管道中运行,因此它们最终会因处理器的问题(和/或退役)宽度而成为瓶颈。通常,这意味着您每个时钟可以执行3到5个NOP。
NOP仍然会占用µop缓存中的空间,这意味着降低了代码密度和缓存效率。
在许多方面,您可以将a NOP
视为等同于a,XOR reg, reg
或者MOV
由于寄存器重命名而在前端被省略。
Skylake 一般可以在一个周期内执行4 个单字节 nop。至少在 Sandy Bridge(以下简称 SnB)微架构中确实如此。
Skylake 和其他回到 SnB 的系统通常也能够在一个周期内执行四个长于 1 字节的数据nop
,除非它们太长而遇到前端限制。
现有的答案更加完整,并解释了为什么您可能不想使用此类单字节nop
指令,因此我不会添加更多内容,但我认为有一个答案能够清楚地回答标题问题是很好的。