【问题标题】:Is performance reduced when executing loops whose uop count is not a multiple of processor width?执行 uop 计数不是处理器宽度倍数的循环时性能会降低吗?
【发布时间】:2024-01-20 20:46:01
【问题描述】:

我想知道各种大小的循环如何在最近的 x86 处理器上执行,作为 uop 数量的函数。

这是 Peter Cordes 在another question 中提出非 4 计数问题的引述:

我还发现循环缓冲区外的 uop 带宽不是 如果循环不是 4 uop 的倍数,则每个循环恒定 4。 (IE。 它是 abc, abc, ...;不是 abca,bcab,...)。 Agner Fog 的微架构文档 不幸的是,并不清楚循环缓冲区的这种限制。

问题在于循环是否需要是 N uop 的倍数才能以最大 uop 吞吐量执行,其中 N 是处理器的宽度。 (即最近的英特尔处理器为 4)。在谈论“宽度”和计算 uops 时,有很多复杂的因素,但我主要想忽略这些。特别是,假设没有微观或宏观融合。

Peter 给出了以下循环示例,其中包含 7 个微指令:

一个 7-uop 循环将发出 4|3|4|3|... 我没有测试过更大的组 循环(不适合循环缓冲区)以查看是否有可能 下一次迭代发出的第一条指令 group 作为它的分支,但我认为不是。

更一般的说法是,在其主体中包含x uops 的循环的每次迭代将至少需要ceil(x / 4) 迭代,而不仅仅是x / 4

对于部分或所有最新的 x86 兼容处理器是否如此?

【问题讨论】:

  • @dwelch:要对此进行微基准测试,您只需编写一个具有 2 个 NOP 的循环与一个具有 3 个 NOP 的循环(加上一个非宏融合的 dec/jnz)。当您从循环中的 4 微指令变为 5 微指令时,总周期应该翻倍。或者只是独立的 reg-reg ALU 操作,如 ADD 或 OR,而不是 NOP。或者你在谈论指令获取?这个实验的重点是测试现代 Intel CPU 中的循环缓冲区,对于微小的循环,它会在前端的其余部分和发布阶段之间回收队列的内容,并将其用作循环缓冲区。所以 L1I 和 L0uop 缓存没有被触及。
  • @dwelch:这会对循环的长期吞吐量产生 25% 到 100% 的影响,因此您可以对持续约 1/10 秒的 100M 次迭代进行基准测试。中断/多任务开销不再是问题。测量很容易:perf stat ./a.out 通过精确的硬件性能计数器为您提供循环计数。您必须知道自己在做什么才能做到这一点,但是 x86 微体系结构内部在此详细级别上已为人所知。与 ARM 相比,不同的微架构要少得多。相同的核心设计从 4W Core-M 扩展到 120W 20 核 Xeon,只是具有不同的 uncore/L3。
  • @dwelch 您的 cmets 完全没有帮助。这是一个真正了解复杂性的人提出的问题。请先阅读 Agner Fog's microarch pdf 中的 Skylake 部分,然后再对为什么这种效果可能难以测量或对齐依赖的原因做出任何错误的猜测。 SnB 家族微体系结构如何创建快捷 NOP,发出它们但不需要将它们分派到执行单元,或多或少都知道。 (不过,这是需要仔细检查的事情,最好尽可能避免)。
  • @dwelch:我没有 SKL,IDK 为什么 BeeOnRope 不只是测试它。顺便说一句,您对 L1I 参与其中是完全错误的。循环缓冲区的全部意义在于它缓存了已经解码的指令,并且可以一次只将它们提供给第 4 阶段的微指令,而无需触及 L1I$ 甚至主 L0 微指令缓存。来自 OS 中断的开销是 1% 的几分之一,这种影响会在运行 100M 次迭代的总周期计数中产生易于测量的 25% 到 100% 的差异。我已经在我的 SnB 硬件上完成了这个,但它的 ATM 坏了,所以我不能自己重新运行实验。
  • There are a lot of complicating factors 如果您在如此低的级别上进行优化,我不确定您是否可以忽略这些复杂因素。当你为一个 CPU 设置了正确的 CPU 时,另一个 CPU 的优化因子会有所不同。

标签: performance assembly x86 cpu-architecture micro-optimization


【解决方案1】:

我对 Linux perf 进行了一些调查,以帮助在我的 Skylake i7-6700HQ 盒子上回答这个问题,Haswell 的结果已由另一位用户提供。以下分析适用于 Skylake,但随后是与 Haswell 的比较。

其他架构可能会有所不同0,为了帮助解决所有问题,我欢迎其他结果。 source is available)。

这个问题主要涉及前端,因为在最近的架构中,前端施加了每个周期四个融合域微指令的硬限制。

循环性能规则总结

首先,我将根据一些“性能规则”来总结结果,以便在处理小循环时牢记。还有很多其他的性能规则——这些是对它们的补充(即,你可能不会为了满足这些规则而违反另一个规则)。这些规则最直接适用于 Haswell 和更高版本的架构 - 请参阅 other answer 了解早期架构的差异概述。

首先,计算循环中 macro-fused 微指令的数量。您可以使用 Agner 的 instruction tables 直接查找每条指令,除了 ALU uop 和紧跟分支通常会融合到一个 uop 中。然后根据这个计数:

  • 如果计数是 4 的倍数,则很好:这些循环以最佳方式执行。
  • 如果计数是偶数且小于 32,则很好,除非是 10,在这种情况下,您应该尽可能展开到另一个偶数。
  • 对于奇数,如果可以的话,您应该尝试展开到小于 32 或 4 的倍数的偶数。
  • 对于大于 32 微指令但小于 64 的循环,如果它还不是 4 的倍数,您可能需要展开:使用超过 64 微指令,您将在 Sklyake 的任何值和几乎所有值下获得高效的性能在 Haswell 上(有一些偏差,可能与对齐有关)。这些循环的低效率仍然相对较小:最需要避免的值是 4N + 1 计数,其次是 4N + 2 计数。

调查结果摘要

对于由 uop 缓存提供的代码,没有明显的 4 倍数效应。任何数量的微指令的循环都可以以每个周期 4 个融合域微指令的吞吐量执行。

对于传统解码器处理的代码,情况正好相反:循环执行时间限制为整数个循环,因此不是 4 微指令的倍数的循环无法达到 4 微指令/循环,因为它们浪费了一些问题/执行槽。

对于从循环流检测器 (LSD) 发出的代码,情况是两者的混合,下面将更详细地解释。一般来说,小于 32 uop 且具有偶数 uop 的循环会以最佳方式执行,而奇数大小的循环则不会,而较大的循环需要 4 uop 的倍数才能以最佳方式执行。

英特尔怎么说

英特尔实际上在他们的优化手册中对此进行了说明,其他答案中的详细信息。

详情

正如任何精通最近的 x86-64 架构的人都知道的那样,在任何时候,前端的获取和解码部分都可能在几种不同的模式下工作,具体取决于代码大小和其他因素。事实证明,这些不同的模式在循环大小方面都有不同的行为。我将在后面单独介绍它们。

旧版解码器

旧版解码器1 是完整的机器码到 uops 解码器,当代码不适合时使用2在 uop 缓存机制(LSD 或 DSB)中。发生这种情况的主要原因是代码工作集大于 uop 缓存(在理想情况下大约为 ~1500 uop,实际上更少)。不过,对于这个测试,我们将利用如果对齐的 32 字节块包含超过 18 条指令3 也将使用传统解码器这一事实。

为了测试旧版解码器的行为,我们使用如下所示的循环:

short_nop:
    mov rax, 100_000_000
ALIGN 32
.top:
    dec rax
    nop
    ...
    jnz .top
    ret

基本上,一个简单的循环倒计时直到rax 为零。所有指令都是单个 uop4 并且 nop 指令的数量是变化的(在显示为 ... 的位置)以测试不同大小的循环(因此 4-uop 循环将有 2 nops,加上两条循环控制指令)。没有宏观融合,因为我们总是将decjnz 与至少一个nop 分开,也没有微观融合。最后,在(隐含的 icache 访问之外)没有内存访问。

请注意,这个循环非常密集 - 每条指令大约 1 个字节(因为 nop 指令每个有 1 个字节) - 所以我们将在 32B 块条件下触发 > 18 条指令只要在循环中达到 19 条指令。根据检查perf 性能计数器lsd.uopsidq.mite_uops,这正是我们所看到的:基本上100% 的指令来自LSD5,直到并包括18 uop 循环,但在 19 uop 及以上,100% 来自传统解码器。

无论如何,以下是从 3 到 99 微秒的所有循环大小的循环/迭代6

蓝点是适合 LSD 的循环,表现出一些复杂的行为。我们稍后会看这些。

红点(从 19 uops/迭代开始)由旧版解码器处理,并显示出非常可预测的模式:

  • 所有带有N uops 的循环都完全采用ceiling(N/4) 迭代

因此,至少对于旧版解码器而言,Peter 的观察完全适用于 Skylake:4 微指令的倍数的循环可能会在 IPC 为 4 时执行,但任何其他数量的微指令都会浪费 1 , 2 或 3 个执行槽(分别用于带有 4N+34N+24N+1 指令的循环)。

我不清楚为什么会发生这种情况。尽管如果您认为解码发生在连续的 16B 块中似乎很明显,因此在 4 微指令/周期循环的解码速率下,不是 4 的倍数在jnz 指令的循环中总是会有一些尾随(浪费)插槽遇到。然而,实际的获取和解码单元由预解码和解码阶段组成,中间有一个队列。预解码阶段实际上有 6 条指令的吞吐量,但每个周期只解码到 16 字节边界的末尾。这似乎意味着循环结束时出现的气泡可能会被预解码器 -> 解码队列吸收,因为预解码器的平均吞吐量高于 4。

因此,根据我对预解码器工作原理的理解,我无法完全解释这一点。可能是在解码或预解码中存在一些额外的限制,以防止非整数循环计数。例如,即使跳转后的指令在预解码队列中可用,传统解码器也可能无法解码跳转两侧的指令。或许和需要handlemacro-fusion有关吧。

上面的测试显示了循环顶部在 32 字节边界上对齐的行为。下面是同一张图,但添加了一个系列,显示循环顶部向上移动 2 个字节时的效果(即,现在在 32N + 30 边界处未对齐):

现在大多数循环大小都会受到 1 或 2 个循环的惩罚。当您考虑解码 16B 边界和每周期解码 4 条指令时,第 1 个惩罚案例是有意义的,并且 2 个周期惩罚案例发生在循环中,由于某种原因,DSB 用于循环中的 1 条指令(可能是 dec 指令它出现在它自己的 32 字节块中),并且会产生一些 DSBMITE 切换惩罚。

在某些情况下,当最终更好地对齐循环末端时,未对齐不会造成伤害。我测试了错位,它以相同的方式持续到 200 个 uop 循环。如果你从表面上看预解码器的描述,看起来,如上所述,它们应该能够隐藏一个未对齐的提取气泡,但它不会发生(可能队列不够大)。

DSB(Uop 缓存)

uop 缓存(英特尔喜欢将其称为 DSB)能够缓存大多数中等数量的指令循环。在典型的程序中,您希望大部分指令都在此缓存中提供7

我们可以重复上面的测试,但现在从 uop 缓存中提供 uops。这是将我们的 nop 大小增加到 2 个字节的简单问题,因此我们不再达到 18 条指令的限制。我们在循环中使用 2 字节 nop xchg ax, ax

long_nop_test:
    mov rax, iters
ALIGN 32
.top:
    dec eax
    xchg ax, ax  ; this is a 2-byte nop
    ...
    xchg ax, ax
    jnz .top
    ret

在这里,结果非常简单。对于从 DSB 交付的所有测试循环大小,所需的循环数为 N/4 - 即,循环以最大理论吞吐量执行,即使它们没有 4 微指令的倍数。因此,一般而言,在 Skylake 上,由 DSB 提供的中等大小的循环无需担心确保 uop 计数满足某个特定倍数。

这是一个包含 1,000 个 uop 循环的图表。如果你眯着眼睛,你可以看到 64-uop 之前的次优行为(当循环在 LSD 中时)。在那之后,这是一个直击,4 IPC 一直到 1,000 微欧(有一个大约 900 的光点,这可能是由于我的盒子上的负载):

接下来,我们看看小到足以放入 uop 缓存的循环的性能。

LSD(循环蒸汽探测器)

重要提示:英特尔显然已禁用 Skylake(SKL150 勘误)和 Kaby Lake(KBL095、KBW095 勘误)芯片上的 LSD,通过微码更新和 Skylake- X 开箱即用,由于a bug 与超线程和 LSD 之间的交互有关。对于那些芯片,下图可能不会有高达 64 uop 的有趣区域;相反,它看起来与 64 微秒后的区域相同。

循环流检测器可以缓存高达 64 uop 的小循环(在 Skylake 上)。在英特尔最近的文档中,它更多地被定位为一种节能机制而不是一种性能特性——尽管使用 LSD 肯定没有提到性能方面的缺点。

针对应该适合 LSD 的循环大小运行此程序,我们得到以下循环/迭代行为:

这里的红线是从 LSD 交付的微指令的百分比。对于从 5 到 56 uop 的所有循环大小,它都以 100% 的速度保持平坦。

对于 3 和 4 uop 循环,我们有不同寻常的行为,即分别有 16% 和 25% 的 uop 来自传统解码器。嗯?幸运的是,它似乎并没有影响循环吞吐量,因为这两种情况都达到了 1 个循环/循环的最大吞吐量——尽管人们可以预期一些 MITELSD 转换惩罚。

在 57 和 62 uop 的循环大小之间,从 LSD 传递的 uop 数量表现出一些奇怪的行为 - 大约 70% 的 uop 是从 LSD 传递的,其余的则从 DSB 传递。 Skylake 名义上具有 64-uop LSD,所以这是在超过 LSD 大小之前的某种过渡——也许在 IDQ(实现 LSD)内存在某种内部对齐,仅导致部分命中这个阶段的LSD。这个阶段很短,而且在性能方面,似乎主要是之前的完全 LSD 性能和之后的完全 DSB 性能的线性组合。

让我们看一下 5 到 56 uop 之间的主要结果。我们看到三个不同的区域:

从 3 到 10 微秒的循环:这里的行为很复杂。这是唯一一个我们看到循环计数无法用单次循环迭代的静态行为解释的区域8。范围足够短,很难说是否有模式。 4、6 和 8 微指令的循环都以N/4 个循环以最佳方式执行(与下一个区域的模式相同)。

另一方面,10 微指令的循环在每次迭代中执行 2.66 个循环,使其成为唯一一个在达到 34 微指令或更高的循环大小(异常值除外)之前不会以最佳方式执行的循环大小26)。这对应于类似于4, 4, 4, 3 的重复 uop/循环执行率。对于 5 uop 的循环,每次迭代可以获得 1.33 个循环,非常接近但与理想的 1.25 不同。这对应于4, 4, 4, 4, 3 的执行率。

这些结果很难解释。结果在每次运行中都是可重复的,并且对于诸如将 nop 替换为实际上执行类似mov ecx, 123 的指令之类的更改具有鲁棒性。这可能与每 2 个周期 1 个分支的限制有关,这适用于所有循环,除了那些“非常小”的循环。可能是微指令偶尔排成一列,导致这种限制出现,导致额外的循环。一旦达到 12 uop 或更高,这永远不会发生,因为每次迭代总是至少需要三个周期。

从 11 微秒到 32 微秒的循环:我们看到一个阶梯模式,但周期为 2。基本上,所有具有 偶数 个 uops 的循环都以最佳方式执行 - 即,恰好采用 N/4 个循环。具有奇数个微指令的循环会浪费一个“问题槽”,并且与具有更多微指令的循环占用相同的周期数(即,一个 17 微指令的循环与一个 18 微指令的循环占用相同的 4.5 个循环)。因此,在许多 uop 计数方面,我们的行为优于 ceiling(N/4),并且我们有第一个证据表明 Skylake 至少可以在非整数周期中执行循环。

唯一的异常值是 N=25 和 N=26,这两个值都比预期的要长约 1.5%。它很小但可重现,并且可以在文件中移动函数。这太小了,无法用每次迭代效应来解释,除非它有一个巨大的周期,所以它可能是别的东西。

这里的整体行为与硬件展开循环完全一致(除了 25/26 异常)2 倍。

从 33 到 ~64 uop 的循环:我们再次看到阶梯模式,但周期为 4,平均性能比高达 32 uop 的情况更差。行为正是ceiling(N/4) - 也就是说,与传统解码器情况相同。因此,对于 32 到 64 uop 的循环,LSD 并没有比传统解码器提供明显的优势,就这一特殊限制的前端吞吐量而言。当然,LSD 还有许多其他更好的方法 - 它避免了许多潜在的解码瓶颈,这些瓶颈会出现在更复杂或更长的指令中,并且可以节省功率等。

所有这一切都非常令人惊讶,因为这意味着从 uop 缓存传递的循环通常在前端比从 LSD 传递的循环执行更好,尽管 LSD 通常被定位为严格比 DSB 更好的 uops 来源(例如,作为建议的一部分,尝试保持循环足够小以适合 LSD)。

这是查看相同数据的另一种方法 - 就给定微指令计数的效率损失而言,与每个周期 4 微指令的理论最大吞吐量相比。 10% 的效率命中意味着您只有通过简单的N/4 公式计算的吞吐量的 90%。

这里的整体行为与硬件不进行任何展开是一致的,这是有道理的,因为超过 32 uop 的循环根本无法在 64 uop 的缓冲区中展开。

上面讨论的三个区域颜色不同,至少可以看到竞争效果:

  1. 在其他条件相同的情况下,涉及的微指令数量越多,效率影响就越低。命中是每次迭代只有一次的固定成本,因此较大的循环支付较小的相对成本。

  2. 当您跨入 33+ uop 区域时,效率会大幅提高:吞吐量损失的大小都会增加,并且受影响的 uop 计数会增加一倍。

  3. 第一个区域有些混乱,7 uop 是最差的总体 uop 计数。

对齐

上面的 DSB 和 LSD 分析是针对与 32 字节边界对齐的循环条目,但未对齐的情况似乎在这两种情况下都没有受到影响:与对齐的情况没有实质性差异(除了可能不到 10 微秒的一些小变化,我没有进一步调查)。

以下是 32N-232N+2 的未对齐结果(即循环顶部 2 个字节之前和之后的 32B 边界):

还显示了理想的N/4 行以供参考。

哈斯韦尔

接下来接下来看看之前的微架构:Haswell。这里的号码由用户Iwillnotexist Idonotexist慷慨提供。

LSD + 传统解码管道

首先,来自“密集代码”测试的结果,该测试测试 LSD(对于较小的 uop 计数)和旧式管道(对于较大的 uop 计数,因为指令密度导致循环“退出”DSB。

我们马上就看到了差异,就何时而言,每个架构从 LSD 为密集循环提供微指令。下面我们比较 Skylake 和 Haswell 的 密集 代码短循环(每条指令 1 个字节)。

如上所述,Skylake 循环在 19 uop 处停止从 LSD 传递,正如代码限制的每 32 字节区域 18 uop 所预期的那样。另一方面,Haswell 似乎也停止从 LSD 可靠地为 16-uop 和 17-uop 循环提供数据。我对此没有任何解释。 3 uop 情况下也有区别:奇怪的是,在 3 和 4 uop 情况下,两个处理器都只从 LSD 中提供 一些 uop,但 4 uop 的确切数量是相同的uops,与 3 不同。

不过,我们主要关心的是实际性能,对吧?因此,让我们看看 32 字节对齐的 dense 代码案例的循环/迭代:

这与上面显示的 Skylake 数据相同(未对齐的系列已被删除),Haswell 绘制在旁边。您会立即注意到 Haswell 的模式相似,但不一样。如上,这里有两个区域:

旧版解码

大于 ~16-18 uop(不确定性如上所述)的循环是从旧版解码器提供的。 Haswell 的模式与 Skylake 有所不同。

对于 19-30 uop 的范围,它们是相同的,但在此之后 Haswell 打破了这种模式。 Skylake 使用了 ceil(N/4) 个周期来处理从传统解码器提供的循环。另一方面,Haswell 似乎采用了ceil((N+1)/4) + ceil((N+2)/12) - ceil((N+1)/12) 之类的东西。好的,这很混乱(更短的形式,有人吗?) - 但基本上这意味着虽然 Skylake 以 4*N 循环最佳地执行循环(即,以 4-uops/循环),但这样的循环(本地)通常是 最少 最佳计数(至少在本地) - 执行此类循环比 Skylake 多一个周期。所以实际上你最好在 Haswell 上使用 4N-1 uops 的循环,除了 25% 的这种循环 也是 的形式为 16-1N (31, 47、63 等)需要一个额外的周期。它开始听起来像闰年计算 - 但这种模式可能最好在上面直观地理解。

我不认为这种模式是 内在 uop 在 Haswell 上调度的,所以我们不应该过多地阅读它。好像是用

解释的
0000000000455a80 <short_nop_aligned35.top>:
16B cycle
  1     1 455a80:       ff c8   dec    eax
  1     1 455a82:       90      nop
  1     1 455a83:       90      nop
  1     1 455a84:       90      nop
  1     2 455a85:       90      nop
  1     2 455a86:       90      nop
  1     2 455a87:       90      nop
  1     2 455a88:       90      nop
  1     3 455a89:       90      nop
  1     3 455a8a:       90      nop
  1     3 455a8b:       90      nop
  1     3 455a8c:       90      nop
  1     4 455a8d:       90      nop
  1     4 455a8e:       90      nop
  1     4 455a8f:       90      nop
  2     5 455a90:       90      nop
  2     5 455a91:       90      nop
  2     5 455a92:       90      nop
  2     5 455a93:       90      nop
  2     6 455a94:       90      nop
  2     6 455a95:       90      nop
  2     6 455a96:       90      nop
  2     6 455a97:       90      nop
  2     7 455a98:       90      nop
  2     7 455a99:       90      nop
  2     7 455a9a:       90      nop
  2     7 455a9b:       90      nop
  2     8 455a9c:       90      nop
  2     8 455a9d:       90      nop
  2     8 455a9e:       90      nop
  2     8 455a9f:       90      nop
  3     9 455aa0:       90      nop
  3     9 455aa1:       90      nop
  3     9 455aa2:       90      nop
  3     9 455aa3:       75 db   jne    455a80 <short_nop_aligned35.top>

在这里,我注意到每条指令出现在 16B 解码块 (1-3) 中,以及解码它的周期。规则基本上是最多接下来的 4 条指令被解码,只要它们落在当前的 16B 块中。否则他们必须等到下一个周期。对于 N=35,我们看到在周期 4 中丢失了 1 个解码槽(在 16B 块中只剩下 3 条指令),但否则循环与 16B 边界甚至最后一个周期( 9)可以解码4条指令。

这里是 N=36 的截断图,除了循环结束之外,它是相同的:

0000000000455b20 <short_nop_aligned36.top>:
16B cycle
  1     1 455a80:       ff c8   dec    eax
  1     1 455b20:       ff c8   dec    eax
  1     1 455b22:       90      nop
  ... [29 lines omitted] ...
  2     8 455b3f:       90      nop
  3     9 455b40:       90      nop
  3     9 455b41:       90      nop
  3     9 455b42:       90      nop
  3     9 455b43:       90      nop
  3    10 455b44:       75 da   jne    455b20 <short_nop_aligned36.top>

现在在第三个也是最后一个 16B 块中有 5 条指令要解码,因此需要一个额外的周期。基本上 35 条指令,对于这种特定的指令模式恰好与 16B 位边界更好地对齐,并且在解码时节省了一个周期。这并不意味着 N=35 通常比 N=36 好!不同的指令将具有不同的字节数,并且会以不同的方式排列。一个类似的对齐问题也解释了每 16 个字节所需的额外周期:

16B cycle
...
  2     7 45581b:       90      nop
  2     8 45581c:       90      nop
  2     8 45581d:       90      nop
  2     8 45581e:       90      nop
  3     8 45581f:       75 df   jne    455800 <short_nop_aligned31.top>

这里最后的jne 已滑入下一个 16B 块(如果指令跨越 16B 边界,它实际上是在后一个块中),导致额外的循环丢失。这仅每 16 个字节发生一次。

因此,Haswell 旧解码器的结果可以通过旧解码器完美解释,其行为如 Agner Fog 的microarchitecture doc 中所述。事实上,如果您假设 Skylake 每个周期可以解码 5 条指令(最多提供 5 条微指令)9,它似乎也可以解释 Skylake 的结果。假设它可以,Skylake 的渐近传统解码吞吐量此代码仍然是 4-uops,因为 16 个 nops 块解码 5-5-5-1,而不是 4-4-4-4在 Haswell 上,因此您只能在边缘获得好处:例如,在上面 N=36 的情况下,Skylake 可以解码所有剩余的 5 条指令,而 Haswell 为 4-1,节省了一个周期。

结果是,似乎可以以一种相当简单的方式理解传统解码器的行为,主要的优化建议是继续按摩代码,使其“聪明地”落入 16B 对齐的块中(也许这就像垃圾箱一样是 NP 难的?)。

DSB(又是 LSD)

接下来让我们看一下代码从 LSD 或 DSB 提供的场景 - 通过使用“long nop”测试避免打破每 32B 块 18-uop 的限制,因此保留在 DSB 中。

哈斯维尔 vs Skylake:

注意 LSD 的行为 - 这里 Haswell 在 57 微秒处停止使用 LSD,这与发布的 57 微秒的 LSD 大小完全一致。没有像我们在 Skylake 上看到的那样奇怪的“过渡期”。 Haswell 对于 3 和 4 微指令也有奇怪的行为,其中只有 ~0% 和 ~40% 的微指令分别来自 LSD。

在性能方面,Haswell 通常与 Skylake 保持一致,但有一些偏差,例如,大约 65、77 和 97 微秒,它向上舍入到下一个周期,而 Skylake 始终能够维持 4 微秒/周期甚至当这导致非整数周期数时。 25 和 26 微秒与预期的轻微偏差已经消失。也许 Skylake 的 6 uop 交付率有助于避免 Haswell 因其 4 uop 交付率而遭受的 uop-cache 对齐问题。

其他架构

以下附加架构的结果由用户 Andreas Abel 友情提供,但由于此处的字符数限制,我们将不得不使用另一个答案进行进一步分析。

需要帮助

尽管社区提供了许多平台的结果,但我仍然对比 Nehalem 更老、比 Coffee Lake 更新的芯片(特别是 Cannon Lake,它是一个新的 uarch)的结果感兴趣。生成这些结果的代码is public。此外,are available 上面的结果也在 GitHub 中以 .ods 格式显示。


0 特别是,传统解码器的最大吞吐量在 Skylake 中明显从 4 uop 增加到 5 uop,uop 缓存的最大吞吐量从 4 增加到 6。这两者都可能影响结果描述here。

1 英特尔实际上喜欢将遗留解码器称为 MITE(微指令翻译引擎),可能是因为使用 legacy 实际标记架构的任何部分是一种错误 内涵。

2 从技术上讲,还有另一个更慢的 uops 来源 - MS(微码排序引擎),用于实现任何超过 4 uops 的指令,但我们在这里忽略它,因为我们的循环都不包含微编码指令。

3 这是因为任何对齐的 32 字节块在其 uop 缓存槽中最多可以使用 3 路,并且每个槽最多可容纳 6 个 uop。所以如果你在一个 32B 的块中使用超过 3 * 6 = 18 uops,代码根本无法存储在 uop 缓存中。在实践中可能很少会遇到这种情况,因为代码需要非常密集(每条指令少于 2 个字节)才能触发。

4nop 指令解码为一个 uop,但在执行之前不会被消除(即它们不使用执行端口) - 但仍会占用前端,因此要计入我们感兴趣的各种限制。

5 LSD 是循环流检测器,它直接在 IDQ 中缓存最多 64 (Skylake) uop 的小循环。在早期的架构中,它可以容纳 28 微指令(两个逻辑核心都处于活动状态)或 56 微指令(一个逻辑核心处于活动状态)。

6 我们不能轻易地在这种模式中适应 2 uop 循环,因为这意味着零个 nop 指令,这意味着 decjnz 指令将宏融合,随着 uop 计数的相应变化。相信我的话,所有具有 4 个或更少微指令的循环最多只能执行 1 个循环/迭代。

7 为了好玩,我只是在一小段 Firefox 上运行 perf stat,我打开一个标签并点击了几个 Stack Overflow 问题。对于交付的指令,我从 DSB 获得 46%,从传统解码器获得 50%,从 LSD 获得 4%。这表明,至少对于像浏览器这样的大而复杂的代码,DSB 仍然无法捕获大部分代码(幸运的是,遗留解码器还不错)。

8 我的意思是,所有其他循环计数都可以通过简单地通过在 uops 中采用“有效”积分循环成本来解释(这可能高于实际大小是 uops)并除以 4。对于这些非常短的循环,这是行不通的 - 通过将任何整数除以 4,您无法达到每次迭代 1.333 个周期。换​​句话说,在所有其他区域,成本的形式为 N/4对于某个整数 N。

9 事实上,我们知道 Skylake 可以从传统解码器每个周期提供 5 微指令,但我们不知道这 5 微指令是否可以来自 5 个不同的说明,或者只有 4 个或更少。也就是说,我们希望 Skylake 可以在模式2-1-1-1 中解码,但我不确定它是否可以在模式1-1-1-1-1 中解码。以上结果提供了一些证据,证明它确实可以解码1-1-1-1-1

【讨论】:

  • @IwillnotexistIdonotexist:完美,这两个链接都对我有用。希望 BeeOnRope 也可以得到它们,并将它们变成相同类型的图表。
  • @IwillnotexistIdonotexist - 非常感谢 Haswell 的数字。我上传了上面的第一部分分析,主要涵盖了遗留解码管道。它实际上揭示了 Skylake 的行为 - 遗留管道现在似乎是一个简单的案例,可以通过查看代码如何落在 16B 边界上来解释(主要是?),附加条件是 Skylake 可以解码 5 uops/ 从 5 条指令循环,而 Haswell 中从 4 条指令循环 4。
  • 另外,我承认自己很惊讶 3-uop 循环中 libpfc 的快速 Haswell 实验中,我得到了 ~100%。我怀疑这是因为您将nop(s) 放在dec raxjne 之间。在循环nop dec jne 中,有 3 个 insns/i 问题,但只有 2 个 uops/i,全部由 LSD 提供,模式为 0-4-0-4。在循环dec nop jne 中,3 个 insns/i 问题,3 个 uops/i,全部由 LSD 提供,模式为 0-4-4-4-0-4-4-4。
  • @PeterCordes - 英特尔终于在最新的优化手册中证实了你的“展开”理论:假设一个符合 LSD 条件的循环在循环体中有 23 个微指令。硬件展开循环,使其仍然适合 μop-queue,在这种情况下是两次。因此,μop-queue 中的循环需要 46 μops。 来自第 3.4.2.4 节。
  • @Andreas Abel 在另一条评论(我现在找不到)中提到 Skylake 传统解码器 (MITE) 仍然只有 4 个解码器,只有它们可以产生的微指令数量增加到 5 .
【解决方案2】:

这是原始答案的后续,根据Andreas Abel 提供的测试结果分析另外五种架构的行为:

  • 尼哈勒姆
  • 沙桥
  • 常春藤桥
  • 布罗德韦尔
  • 咖啡湖

除了 Skylake 和 Haswell 之外,我们还快速查看了这些架构的结果。它只需要一个“快速”的外观,因为除了 Nehalem 之外的所有架构都遵循上面讨论的现有模式之一。

首先,使用传统解码器(用于不适合 LSD 的循环)和 LSD 的短 nop 情况。以下是此场景的循环/迭代,适用于所有 7 种架构。

图 2.1:所有架构的密集 nop 性能:

这个图表真的很忙(点击查看大图)并且有点难以阅读,因为许多架构的结果相互重叠,但我试图确保专门的读者可以跟踪任何架构的线路.

首先,让我们讨论一下大异常值:Nehalem。所有其他架构的斜率大致遵循 4 uops/cycle 线,但 Nehalem 几乎恰好是每个周期 3 uops,因此很快落后于所有其他架构。在最初的 LSD 区域之外,这条线也是完全平滑的,没有在其他架构中看到的“阶梯”外观。

这与 Nehalem 的 uop 退休 限制为 3 uops/cycle 完全一致。这是 LSD 之外的微指令的瓶颈:它们都以每个周期大约 3 微指令的速度执行,在退役时成为瓶颈。前端不是瓶颈,因此确切的 uop 计数和解码安排无关紧要,因此不存在阶梯。

除了 Nehalem 之外,除 Broadwell 之外的其他架构都相当干净地分成了几组:Haswell-like 或 Skylake-like。也就是说,所有 Sandy Bridge、Ivy Bridge 和 Haswell 的行为都像 Haswell,对于大于约 15 uop 的循环(Haswell 行为在另一个答案中讨论)。尽管它们是不同的微架构,但它们的行为在很大程度上是相同的,因为它们的传统解码能力是相同的。在大约 15 uop 以下,我们看到 Haswell 对于任何 uop 计数而不是 4 的倍数都要快一些。也许由于 LSD 更大,它在 LSD 中得到了额外的展开,或者还有其他“小循环”优化。对于 Sandy Bridge 和 Ivy Bridge,这意味着小循环绝对应该以 4 的倍数为目标。

Coffee Lake 的行为类似于 Skylake1。这是有道理的,因为微架构是相同的。 Coffee Lake 在大约 16 微秒以下看起来比 Skylake 好,但这只是默认情况下 Coffee Lake 禁用 LSD 的效果。 Skylake 使用启用的 LSD 进行了测试,之后英特尔因安全问题通过微码更新禁用了它。 Coffee Lake 是在已知此问题后发布的,因此开箱即用地禁用了 LSD。因此,对于本次测试,Coffee Lake 使用 DSB(对于低于约 18 uop 的循环,仍然可以放入 DSB)或传统解码器(对于其余的循环),这会为小 uop 数带来更好的结果LSD 施加开销的循环(有趣的是,对于较大的循环,LSD 和传统解码器恰好施加完全相同的开销,原因非常不同)。

最后,我们看一下 2 字节 NOP,它们的密度不足以阻止使用 DSB(因此这种情况更能反映典型代码)。

图 2.1:2 字节 nop 性能:

同样,结果与之前的图表相同。 Nehalem 仍然是每个周期 3 微秒的异常值。对于高达大约 60 微秒的范围,除了 Coffee Lake 之外的所有架构都使用 LSD,我们看到 Sandy Bridge 和 Ivy Bridge 在这里的表现稍差,四舍五入到下一个周期,因此只能达到 4 的最大吞吐量如果循环中的 uops 数是 4 的倍数,则为 uops/cycle。超过 32 uops 时 Haswell 的“展开”功能和新的 uarch 没有任何效果,所以一切都大致绑定。

Sandy Bridge 实际上有一些 uop 范围(例如,从 36 到 44 uop),在这些范围内它的性能优于新的架构。这似乎是因为并非所有循环都被 LSD 检测到,并且在这些范围内,循环由 DSB 提供服务。由于 DSB 通常更快,因此在这些情况下 Sandy Bridge 也更快。

英特尔怎么说

正如 Andreas Abel 在 cmets 中指出的那样,您实际上可以在英特尔优化手册第 3.4.2.5 节中找到专门处理此主题的部分。在那里,英特尔说:

LSD 拥有构建小的“无限”循环的微操作。 来自 LSD 的微操作在乱序引擎中分配。这 LSD 中的循环以一个到循环开头的分支结束。 循环结束时采用的分支始终是最后一个微操作 在循环中分配。循环开始的指令 总是在下一个周期分配。如果代码性能是 受前端带宽的限制,未使用的分配槽会导致 分配中的泡沫,并可能导致性能下降。 英特尔微架构代号 Sandy Bridge 中的分配带宽 每个周期有四个微操作。性能最好,当数 LSD 中的微操作导致最少数量的未使用分配 插槽。您可以使用循环展开来控制微操作的数量 在 LSD 中。

他们继续展示了一个示例,其中由于 LSD“四舍五入”,将循环展开两倍并不能提高性能,但展开三倍有效。这个例子很令人困惑,因为它实际上混合了两种效果,因为展开更多也减少了循环开销,从而减少了每次迭代的微指令数。一个更有趣的例子是,由于 LSD 舍入效应,展开循环 更少 次导致性能提高。

这部分似乎准确地描述了 Sandy Bridge 和 Ivy Bridge 中的行为。上面的结果表明,这两种架构都按照描述的方式运行,对于 4N+3、4N+2 或 4N+1 uop 的循环,您分别损失了 1、2 或 3 个 uop 执行槽。

它尚未更新 Haswell 及更高版本的新性能。如另一个答案中所述,性能比上述简单模型有所提高,并且行为更加复杂。


1 在 16 uop 处有一个奇怪的异常值,其中 Coffee Lake 的性能比所有其他架构都差,甚至是 Nehalem(大约 50% 的回归),但可能是这种测量噪声?

【讨论】:

  • @Peter 这是密集的 nop 情况,因此通常使用传统解码器,因为每个 uop 缓存行的指令太多。然而,对于像 18 岁以下的小循环,希望人们可以想象仍然可以使用 uop 缓存,因为没有足够的 nop 来“突破”——这是我在启用 LSD 的 Sklyake 上看到的。然而,对于咖啡湖的结果,即使基于 perf 计数器结果的那些小循环,似乎也没有使用 DSB。
  • 稍后我将在 Coffee Lake 上再次运行测试,看看异常值是否是测量错误。
  • 我刚刚看到英特尔优化手册的第 3.4.2.5 节。到目前为止,答案中没有提到它,但它似乎与这里讨论的问题有关。
  • @PeterCordes - 对上述内容的更正:Coffee Lake 确实将 DSB 用于小于约 18 微欧的小循环,即使在“密集”情况下也是如此,所以一切都符合预期(我也观察到了这一点在 Skylake 微码前补丁上,除了用 LSD 替换 DSB)。我只是读错了数据或记错了。是的,似乎 LSD 策略在 Haswell 中可能得到了改进:也许当时添加了整个“展开”的东西,所以在那之前,当它们不是 4N 形式时,小循环尤其受到影响。这使得展开对于这些架构更加重要。
  • 我添加了为 Coffee Lake 创建了一个新的拉取请求,其中包含额外的结果。 16 微秒的异常值是测量错误,可能是由超线程引起的。
【解决方案3】:

TL;DR:对于正好由 7 个微指令组成的紧密循环,它会导致低效的引退带宽利用率。考虑手动展开循环,这样循环将包含 12 个微指令


我最近遇到了由 7 个微指令组成的循环的退休带宽下降问题。在自己做了一些研究之后,快速谷歌搜索将我引向这个话题。这是我申请 Kaby Lake i7-8550U CPU 的 2 美分:

正如@BeeOnRope 所说,LSD 在像KbL i7-8550U 这样的芯片上是关闭的。

考虑以下 NASM 宏

;rdi = 1L << 31
%macro nops 1
    align 32:
    %%loop:
    times %1 nop
    dec rdi
    ja %%loop
%endmacro

“平均退休率”uops_retired.retire_slots/uops_retired.total_cycle 如下所示:

这里要注意的是当循环由 7 个微指令组成时,退役退化。它导致每个周期有 3.5 微指令被淘汰。

idq 的平均投递率idq.all_dsb_cycles_any_uops / idq.dsb_cycles 看起来像

对于 7 微指令的循环,它会导致每个周期将 3.5 微指令传送到 idq。仅凭此计数器判断,无法断定 uops 缓存提供 4|3 还是 6|1 组。

对于由 6 个微指令组成的循环,它可以有效利用微指令缓存带宽 - 6 微指令/c。当 IDQ 溢出时,微指令缓存保持空闲状态,直到它可以再次提供 6 个微指令。

要检查 uops 缓存如何保持空闲,让我们比较 idq.all_dsb_cycles_any_uops 和周期

uop 传送到 idq 的周期数等于 7 uop 的循环的总周期数。相比之下,6 微指令循环的计数器明显不同。

要检查的关键计数器是idq_uops_not_delivered.*

从 7 条微指令的循环中可以看出,重命名器需要 4|3 个组,这会导致退休带宽利用率低下。

【讨论】:

  • 在寻找瓶颈时,在查看性能计数器时,我会小心因果关系的假设。从一开始,您就有一些瓶颈,导致持续吞吐量为 3.5 uops/cycle。这里所说的“瓶颈”只是指您没有以最大理论 4.0 uops 周期运行。即使不知道瓶颈的来源,必须管道中的每个性能计数器:前端、分配、调度、发布、退休,都将报告完全相同的 3.5 持续吞吐量。
  • ... 在这种情况下有一个轻微的例外,因为您使用了 nop,它不会执行。因此,每个计数器都会报告小于最大带宽、有未使用的周期或插槽等。这并不能告诉您 为什么 存在瓶颈。如果你有一个执行瓶颈,比如一串相关的乘法指令,所有的前端计数器都会报告非常少的交付的微指令,以及大量的空闲周期等等,尽管存在零 FE 问题:它不能否则:在稳定状态下,管道每一部分的吞吐量必须相等。
  • 因此,您通常不能使用 DSB 计数器得出 DSB 导致瓶颈的结论。大多数其他柜台也是如此。这就是为什么 VTune 的方法需要“条件”计数器:诸如“没有从前端传递 uop 并且 分配没有停滞的周期”。也就是说,如果 RAT 能够 接受操作,但 FE 不能提供它们:在这种情况下,您可以合理地认为您可能会遇到问题。
  • 无论如何,下降到 7 uop 的原因是相当清楚的:DSB 每个周期只能从一条 6 uop 线传送,并且不能有效地通过采取的跳转来传送(uop 缓存是不是跟踪缓存)。因此,一个 7 uop 循环将始终需要至少 2 个周期:因为您需要 2 个周期来交付 7 uop。
  • 7 微秒 / 2 周期 = 3.5 / 周期。对于 6 个 uops,没有问题:所有 uops 都可以来自单一方式(如果满足其他限制),因此您在其他地方被限制为 4 个/循环。对于 8 微指令,您还需要 2 个周期,但 8 / 4 = 2 所以您不会真正注意到瓶颈。顺便说一句,这也是为什么将 DSB 线大小增加到 6 uop 很有用的一个原因:因此具有 5 或 6 uop 的循环可以从 DSB 以 4 uop/cycle 的速度执行。
最近更新 更多