8

我正在将分支目标与 NOP 对齐,有时 CPU 会执行这些 NOP,最多 15 个 NOP。Skylake 在一个周期内可以执行多少个 1 字节的 NOP?其他与 Intel 兼容的处理器(如 AMD)呢?我不仅对 Skylake 感兴趣,还对其他微架构感兴趣。执行 15 个 NOP 的序列可能需要多少个周期?我想知道添加这些 NOP 的额外代码大小和额外执行时间是否物有所值。这不是我添加这些 NOP 的人,而是我在编写align指令时自动添加的一个汇编程序。

更新:我已经管理汇编器NOP自动插入多字节。

4

3 回答 3

6

添加这些 NOP 的不是我,而是一个汇编程序。它非常愚蠢并且不支持对齐选项(BASM) - 只有一个选项 - 边界大小。

我不知道“BASM”是什么,我在网上找不到任何对它的引用(除了this,它显然不是 x86),但如果它不支持多字节 NOP,你真的需要一个不同的汇编程序。这只是多年来一直在英特尔和 AMD 架构手册中的基本内容。Gnu 汇编器可以为 ALIGN 指令执行此操作,Microsoft 的 MASM 也可以。开源的NASMYASM汇编器也支持这一点,并且它们中的任何一个都可以轻松地集成到任何现有的构建系统中。

多字节 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代替INCLEA代替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 nnLEA可以给出类似的指令LEA EBX,[EBX]通过不同地添加一个 SIB 字节、一个段前缀和一个或四个零字节的偏移量,可以得到从 2 到 8 的任意长度。不要在 32 位模式下使用两字节偏移,因为这会减慢解码速度。并且不要使用多个前缀,因为这会减慢旧英特尔处理器的解码速度。

使用伪 NOP(例如MOV RAX,RAXLEA 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, regMOV由于寄存器重命名而在前端被省略。

于 2017-07-12T18:11:04.370 回答
4

另请参阅 Cody 的回答,了解我遗漏的很多好东西,因为他已经涵盖了它。


切勿使用多个 1 字节 NOP。所有的汇编器都有办法获得长 NOP;见下文。

15 个 NOP 需要 3.75c 以通常每个时钟发出 4 个,但如果此时它在长依赖链上遇到瓶颈,可能根本不会减慢您的代码。他们确实在 ROB 中一直占用空间,直到退休。他们唯一不做的就是使用执行端口。关键是,CPU 性能不是累加的。您不能只说“这需要 5 个周期,而这需要 3 个,所以它们加起来需要 8 个”。乱序执行的要点是与周围的代码重叠。

许多 1 字节短 NOP 对 SnB 系列的更坏影响是它们倾向于溢出每个对齐的 32B 块 x86 代码 3 行的 uop-cache 限制。这意味着整个 32B 块始终必须从解码器运行,而不是从 uop 缓存或循环缓冲区运行。(循环缓冲区仅适用于所有 uop 都在 uop 缓存中的循环)。

您应该只在一行中最多有 2 个实际执行的 NOP,然后只有当您需要填充超过 10B 或 15B 或其他内容时。(某些 CPU 在解码具有很多前缀的指令时表现非常糟糕,因此对于实际执行的 NOP,最好不要将前缀重复到 15B(最大 x86 指令长度)。


YASM 默认设置长 NOP。对于 NASM,使用标准smartalign宏包,默认情况下不启用。它迫使您选择 NOP 策略。

%use smartalign
ALIGNMODE p6, 32     ;  p6 NOP strategy, and jump over the NOPs only if they're 32B or larger.

IDK 如果 32 是最优的。另外,请注意最长的 NOP 可能会使用大量前缀,并且在 Silvermont 或 AMD 上解码缓慢。查看 NASM 手册了解其他模式。

GNU 汇编程序的.p2align指令为您提供了一些条件行为.p2align 4,,10将对齐到 16 (1<<4),但前提是跳过 10 个字节或更少。(空的第二个参数表示填充符是 NOP,2 的幂对齐名称是因为 plain.align在某些平台上是 2 的幂,但在其他平台上是字节数)。gcc 经常在循环顶部之前发出这个:

  .p2align 4,,10 
  .p2align 3
.L7:

所以你总是得到 8 字节对齐(无条件.p2align 3),但也可能是 16,除非那会浪费超过 10B。将较大的对齐放在首位对于避免获得例如 1 字节 NOP 然后是 8 字节 NOP 而不是单个 9 字节 NOP 很重要。

可能可以使用 NASM 宏来实现此功能。


缺少汇编程序没有的功能(AFAIK)

  • 通过使用更长的编码(例如 imm32 而不是 imm8 或不需要的 REX 前缀)来填充前面指令的指令,以在没有 NOP 的情况下实现所需的对齐。
  • 基于以下指令长度的智能条件内容,例如如果可以在到达下一个 16B 或 32B 边界之前解码 4 条指令,则不填充。

解码瓶颈的对齐通常不再很重要,这是一件好事,因为调整它通常涉及手动组装/反汇编/编辑周期,并且如果前面的代码发生更改,则必须再次查看。


特别是如果您有机会为一组有限的 CPU 进行调优,如果您没有发现性能优势,请进行测试并且不要填充。在很多情况下,特别是对于具有 uop 缓存和/或循环缓冲区的 CPU,可以不对齐函数内的分支目标,甚至循环。


由于不同的对齐方式导致的一些性能变化是它使不同的分支在分支预测缓存中相互别名。 即使 uop 缓存完美运行并且从 uop 缓存中获取大部分为空的行没有前端瓶颈,这种次要的微妙效果仍然存在。

另请参阅x86-64 程序集的性能优化 - 对齐和分支预测

于 2017-07-13T04:18:54.930 回答
4

Skylake 一般可以在一个周期内执行四个单字节 nop 。至少可以追溯到 Sandy Bridge(以下称为 SnB)微架构时,情况就是如此。

Skylake 和其他回到 SnB 的人,通常也能够nop在一个周期内执行四个长于一字节的 s,除非它们长到遇到前端限制。


现有的答案更加完整,并解释了为什么您可能不想使用这样的单字节nop指令,所以我不会添加更多,但我认为有一个答案可以清楚地回答标题问题是很好的。

于 2017-07-15T19:34:49.400 回答