【问题标题】:How many 1-byte NOPs can Skylake execute at one cycleSkylake 一个周期可以执行多少个 1 字节 NOP
【发布时间】:2017-12-15 21:34:26
【问题描述】:

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

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

【问题讨论】:

  • 看看Agner Fog's tables。它应该为您提供所需的数字。
  • @fuz - 它告诉 0.25,即每个周期 4 NOPs?这很慢!
  • 听起来不错!考虑使用多字节 nops(操作码 0f 1f /0)来获得每个周期的更多 nops。
  • @fuz - 我不能 - 每当我写 '.align 16' 时,放 NOP 的不是我,而是汇编程序 - 我不倾向于手动放 NOP,因为这样会很乏味当我更改代码时重新对齐。在执行 NOP 时,我可能应该在某处使用 '.align 4',而不是 '.align 16',即遵循像 jz 这样的条件跳转,而不是像 `jmp' 这样的无条件跳转。
  • GNU 汇编器可以选择自动生成长 nop。

标签: assembly optimization x86 alignment nop


【解决方案1】:

添加这些 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 字节形式的指令,例如 INCDECPUSHPOP 等,而不是短版本
  • 使用更长的等效指令,例如 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 不会降低性能,因为它们永远不会被执行。

对齐循环条目的问题更大,因为前面的代码也被执行了。最多可能需要 15 个NOP 来将循环条目对齐 16。这些NOP 将在进入循环之前执行,这将花费处理器时间。使用更长的指令比使用大量单字节NOP 更有效。最好的现代汇编程序会这样做,并使用像 MOV EAX,EAXLEA EBX,[EBX+00000000H] 填充 ALIGN nn 语句之前的空格。 LEA 指令特别灵活。可以通过不同地添加一个 SIB 字节、一个段前缀和一个或四个零字节的偏移量来给出类似 LEA EBX,[EBX] 的任何长度的指令,从 2 到 8。不要在 32 位模式下使用两字节偏移,因为这会减慢解码速度。并且不要使用多个前缀,因为这会减慢旧英特尔处理器的解码速度。

使用MOV RAX,RAXLEA RBX,[RBX+0]之类的伪NOP作为填充符的缺点是对寄存器有错误的依赖,会占用执行资源。最好使用多字节 NOP 指令,可以调整到所需的长度。多字节 NOP 指令适用于所有支持条件移动指令的处理器,即 Intel PPro、P2、AMD Athlon、K7 及更高版本。

对齐循环条目的另一种方法是将前面的指令编码为比必要更长的方式。在大多数情况下,这不会增加执行时间,但可能会增加指令获取时间。

他还继续展示了通过移动前面的子例程条目来对齐内部循环的另一种方法的示例。这有点尴尬,即使在最好的汇编程序中也需要进行一些手动调整,但它可能是最优化的机制。同样,这仅在热路径上的关键内部循环中很重要,您可能已经在其中进行了挖掘和微优化。

有趣的是,我已经对正在优化的代码进行了多次基准测试,但并没有发现对齐循环分支目标有什么好处。例如,我正在编写一个优化的strlen 函数(Gnu 库有,但微软没有),并尝试将主内循环的目标对齐在 8 字节、16 字节和 32 字节边界上。这些都没有太大的不同,尤其是与我在重写代码时取得的其他重大性能进步相比时。

请注意,如果您不针对特定处理器进行优化,您可能会疯狂地尝试寻找最佳的“通用”代码。当谈到对齐对速度的影响时,things can vary wildly。糟糕的对齐策略通常比没有对齐策略更糟糕。

二次幂边界总是一个好主意,但这很容易实现,无需任何额外的努力。同样,不要忽视对齐,因为它可能很重要,但出于同样的原因,不要执着于尝试对齐每个分支目标。

在最初的 Core 2(Penryn 和 Nehalem)微架构上,对齐曾经是一个更大的问题,其中严重的解码瓶颈意味着,尽管问题宽度为 4,但您很难保持其执行单元忙碌。随着在 Sandy Bridge 中引入 µop 缓存(Pentium 4 中为数不多的好功能之一,最终被重新引入 P6 扩展系列),前端吞吐量显着增加,这变得不那么简单了。问题。

坦率地说,编译器也不擅长进行这些类型的优化。 GCC 的-O2 开关意味着-falign-functions-falign-jumps-falign-loops-falign-labels 开关,默认首选项为在8 字节边界上对齐。这是一种非常直率的方法,而且里程数各不相同。正如我在上面链接的那样,关于禁用这种对齐并使用紧凑代码是否实际上可以提高性能的报告各不相同。此外,您将看到编译器所做的最好的事情就是插入多字节 NOP。我还没有看到使用较长形式的指令或为了对齐目的而大幅重新排列代码的方法。所以我们还有很长的路要走,这是一个非常难以解决的问题。 Some people are working on it,但这只是表明问题的真正棘手程度:“指令流中的微小变化,例如插入单个 NOP 指令,可能会导致显着的性能差异,并具有暴露的效果” (请注意,虽然很有趣,但那篇论文来自早期的 Core 2 天,正如我之前提到的,它遭受了比大多数人更多的错位惩罚。我不是确定你是否会在今天的微架构上看到同样巨大的改进,但我不能肯定地说,因为我还没有进行测试。也许谷歌会雇用我,我可以发表另一篇论文?)

Skylake 在一个周期内可以执行多少个 1 字节 NOP?其他与 Intel 兼容的处理器(如 AMD)呢?我不仅对 Skylake 感兴趣,还对其他微架构感兴趣。执行 15 个 NOP 的序列可能需要多少个周期?

可以通过查看 Agner Fog 的 instruction tables 并搜索 NOP 来回答此类问题。我不会费心将他的所有数据提取到这个答案中。

不过,一般来说,只要知道 NOP 不是免费的即可。尽管它们不需要执行单元/端口,但它们仍然必须像任何其他指令一样通过管道运行,因此它们最终会受到处理器问题(和/或退役)宽度的限制。这通常意味着您可以在每个时钟执行 3 到 5 个 NOP。

NOP 仍然会占用 µop 缓存中的空间,这意味着代码密度和缓存效率会降低。

在许多方面,您可以将NOP 视为等同于XOR reg, regMOV,由于寄存器重命名而在前端被省略。

【讨论】:

  • 感谢您的出色回复!我已经管理汇编程序自动输入多字节nops。我指定对齐 2 到 16 个字节,具体取决于上下文和重要性,但是,总的来说,我尝试在对齐之后,至少有两条指令适合边界。所以,如果它只是两个pop,我会对齐 2,但如果有一个重要的 AVX 循环来复制内存,我会对齐 16。我同意你的推理,即丢失空间和时间处理这些 NOP,即使是多字节 NOP 也可能不值这个价,尤其是当代码变大变短jzs 变长时。
  • @MaximMasiutin:如果你想要那种对齐的灵活性,GNU 汇编器可能是一个不错的选择。 .p2align 4,,10 将与 16 (1.p2align 4,,10 ; .p2align 3 一个接一个,所以你总是得到 8 字节对齐,但也可能是 16,除非这会浪费大部分 16B。但是由于没有汇编程序会为您填充指令并完全避免 NOP,因此您可能必须自己做。
  • 我的汇编器对多字节 NOPs 使用略有不同的操作码 - 这些是各种 LEA RAX/EAX,有或没有 FS 段前缀字节 (64h)
【解决方案2】:

Skylake 一般可以在一个周期内执行四个单字节nops。至少可以追溯到 Sandy Bridge(以下简称 SnB)微架构。

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


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

【讨论】:

    【解决方案3】:

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


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

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

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

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


    YASM 默认设置长 NOP。对于 NASM,使用the smartalign standard macro package,默认情况下不启用。它迫使您选择 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.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 的情况下实现所需的对齐。
    • 基于后续指令长度的智能条件内容,例如如果 4 条指令可以在到达下一个 16B 或 32B 边界之前解码,则不填充。

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


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


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

    另见Performance optimisations of x86-64 assembly - Alignment and branch prediction

    【讨论】:

    • “特别是如果您有机会为有限的 CPU 进行调优……” 我会得出与您在此处所做的相同的结论,但情况恰恰相反!你不可能在每一个 CPU 上进行测试,所以总会有一些你的代码运行得不是最优的。最好只为一般情况做出好的、常识性的选择,这通常意味着不要为了对齐目的而插入 NOP。另外,我认为下一个粗体声明,关于性能差异是由于不同分支在 BP 中相互别名造成的,是我引用的那篇论文中缺少的分析。
    • 无论如何,很好的答案。感谢您填写一些我忽略或忘记的细节,例如如何在 NASM 中使用 smartalign 以及 .p2align 如何在 Gas 中工作。我认为看到汇编器在指令上工作以选择更长的指令编码以用于填充/对齐原因会非常有趣。我想知道这是否是 NASM 或 YASM 人员有兴趣研究的东​​西?似乎常见的候选指令映射可以是表驱动的,这足以在很多情况下产生影响。前缀会更容易自动插入。
    • @CodyGray:前缀(REX 除外)的风险在于未来的 CPU 可能赋予它们不同的含义。例如rep bsf 在较新的 CPU 上是 tzcnt。不过,我认为 REX.W=0 应该始终是安全的,除了使用 AH/.../DH 的指令。 (还必须检查您最终的前缀总数是否不超过 3 个,否则 Silvermont/KNL 将在解码时停止。)
    • 对于它的价值,我最近一直在 Skylake 上研究循环对齐,从经验上看,对齐 16 或更多似乎几乎不值得,主要是因为各种前端部件对齐帮助最大的人都在变得更好,并且不太常见的瓶颈。事实上,对于任何给定的循环,我经常发现 align-by-16 比其他几个随机对齐要慢(通常有 2 或 3 个性能级别,定期重复)。
    • 最大的罪魁祸首似乎是分支预测行为,尤其是对于嵌套循环,以及调度程序端口绑定行为,特别是对于具有端口争用的高 IPC 代码。例如,如果计划正确,您可能有代码应该 4 命中 IPC,但实际上它只在每 20 个对齐或其他任何对齐中到达那里,而不一定是“偶数”对齐。这种行为很难控制,因为它似乎依赖于许多地址位,这些地址位可能会在不相关的代码更改时发生变化。
    猜你喜欢
    • 2017-01-31
    • 2014-02-13
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-02-02
    • 2016-09-02
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多