执行 uop 计数不是处理器宽度倍数的循环时性能会降低吗?
Is performance reduced when executing loops whose uop count is not a multiple of processor width?
我想知道各种大小的循环在最近的 x86 处理器上如何执行,作为 uops 数的函数。
引用 Peter Cordes 的话,他在 another question 中提出了非 4 的倍数计数问题:
I also found that the uop bandwidth out of the loop buffer isn't a
constant 4 per cycle, if the loop isn't a multiple of 4 uops. (i.e.
it's abc, abc, ...; not abca, bcab, ...). Agner Fog's microarch doc
unfortunately wasn't clear on this limitation of the loop buffer.
问题是关于循环是否需要是 N 微指令的倍数才能以最大微指令吞吐量执行,其中 N 是处理器的宽度。 (即,最近的 Intel 处理器为 4)。在谈论 "width" 和计数 uops 时有很多复杂的因素,但我主要想忽略这些。特别是,假设没有微观或宏观融合。
Peter 给出了以下循环体中包含 7 微指令的循环示例:
A 7-uop loop will issue groups of 4|3|4|3|... I haven't tested larger
loops (that don't fit in the loop buffer) to see if it's possible for
the first instruction from the next iteration to issue in the same
group as the taken-branch to it, but I assume not.
更一般地说,声明的是在其主体中具有 x
微指令的循环的每次迭代将至少进行 ceil(x / 4)
次迭代,而不是简单地 x / 4
。
部分或所有最新的 x86 兼容处理器是否如此?
我用 Linux perf
做了一些调查,以帮助在我的 Skylake i7-6700HQ 盒子上回答这个问题,另一位用户友情地提供了 Haswell 结果。下面的分析适用于 Skylake,但随后与 Haswell 进行了比较。
其他架构可能会有所不同0,为了帮助解决所有问题,我欢迎提供更多结果。 source is available).
这个问题主要涉及前端,因为在最近的架构中,前端强加了每个周期四个 fused-domain 微指令的硬限制。
循环性能规则总结
首先,我将根据一些 "performance rules" 来总结结果,以便在处理小循环时牢记。还有很多其他性能规则 - 这些是它们的补充(即,您可能不会为了满足这些规则而违反另一条规则)。这些规则最直接适用于 Haswell 和后来的架构 - 请参阅 了解早期架构差异的概述。
首先,计算循环中 macro-fused 微指令的数量。您可以使用 Agner 的 instruction tables 直接查找每条指令,除了 ALU uop 和紧随其后的 b运行ch 通常会融合在一起成为一个 uop。然后根据这个计数:
- 如果计数是 4 的倍数,那很好:这些循环执行得最好。
- 如果计数是偶数且小于 32,那很好,除非是 10,在这种情况下,如果可以的话,您应该展开到另一个偶数。
- 对于奇数,如果可以的话,您应该尝试展开到小于 32 或 4 的倍数的偶数。
- 对于大于 32 微指令但小于 64 的循环,如果它不是 4 的倍数,您可能想要展开:超过 64 微指令,您将在 Sklyake 上获得任何值的高效性能,几乎Haswell 上的所有值(有一些偏差,可能与对齐有关)。这些循环的低效率仍然相对较小:最应避免的值是
4N + 1
计数,其次是 4N + 2
计数。
调查结果摘要
对于由 uop 缓存提供的代码,没有明显的 multiple-of-4 效果。可以以每周期 4 fused-domain 微指令的吞吐量执行任意数量微指令的循环。
对于传统解码器处理的代码,情况恰恰相反:循环执行时间被限制为循环的整数倍,因此不是 4 微指令倍数的循环无法达到 4 uops/cycle,如他们浪费了一些 issue/execution 个位置。
对于从循环流检测器 (LSD) 发出的代码,情况是两者的混合,下面将对此进行更详细的解释。通常,小于 32 微指令和偶数微指令的循环最佳执行,而 odd-sized 循环则不然,更大的循环需要 multiple-of-4 微指令才能最佳执行。
英特尔的说法
英特尔实际上在他们的优化手册中对此有说明,其他答案中有详细说明。
详情
任何人 well-versed 最近的 x86-64 架构都知道,在任何时候,前端的提取和解码部分都可能以一种不同的模式工作,具体取决于代码大小和其他因素。事实证明,这些不同的模式在循环大小调整方面都有不同的行为。我将在后面分别介绍它们。
旧版解码器
legacy decoder1是使用的完整machine-code-to-uops解码器2 当代码不适合 uop 缓存机制(LSD 或 DSB)时。发生这种情况的主要原因是代码工作集大于 uop 缓存(在理想情况下约为 1500 uops,在实践中更少)。不过,对于此测试,我们将利用以下事实:如果对齐的 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 nop
s,加上两个循环控制指令)。没有 macro-fusion,因为我们总是将 dec
和 jnz
与至少一个 nop
分开,也没有 micro-fusion。最后,在(隐含的 icache 访问之外)没有内存访问。
请注意,此循环非常密集 - 每条指令约 1 个字节(因为 nop
指令每个为 1 个字节) - 因此我们将触发 > 32B 块条件下的 18 条指令一旦在循环中命中 19 条指令。基于检查 perf
性能计数器 lsd.uops
和 idq.mite_uops
这正是我们所看到的:基本上 100% 的指令来自 LSD 直到并包括 18 微指令循环,但在 19 微指令及以上,100% 来自传统解码器。
无论如何,这是从 3 到 99 uops 的所有循环大小的 cycles/iteration6:
蓝点是适合 LSD 的循环,表现出一些复杂的行为。我们稍后再看这些。
红点(从 19 uops/iteration 开始)由传统解码器处理,显示出非常可预测的模式:
- 所有具有
N
微指令的循环正好进行 ceiling(N/4)
次迭代
因此,至少对于传统解码器而言,Peter 的观察在 Skylake 上完全成立:4 uops 的倍数 的循环可能以 4 的 IPC 执行,但任何其他数字of uops 将浪费 1、2 或 3 个执行槽(分别用于具有 4N+3
、4N+2
、4N+1
指令的循环)。
我不清楚为什么会这样。虽然如果你考虑解码发生在连续的 16B 块中,这似乎很明显,因此在 4 uops/cycle 循环的解码速率下,不是 4 的倍数总是在循环中有一些尾随(浪费的)槽 jnz
遇到指令。然而,实际的提取和解码单元是由预解码和解码阶段组成的,有一个队列in-between。预解码阶段实际上具有 6 条指令的吞吐量,但每个周期仅解码到 16 字节边界的末尾。这似乎意味着循环结束时出现的气泡可以被预解码器吸收 -> 解码队列,因为预解码器的平均吞吐量高于 4.
所以我无法根据我对预解码器工作原理的理解来完全解释这一点。可能是在解码或 pre-decoding 中有一些额外的限制阻止了 non-integral 循环计数。例如,即使跳转后的指令在预解码队列中可用,遗留解码器也可能无法解码跳转两侧的指令。可能跟需要handle macro-fusion.
有关
上面的测试显示了循环顶部在 32 字节边界上对齐的行为。下面是同一张图,但添加了一个系列,显示了循环顶部向上移动 2 个字节时的效果(即,现在在 32N + 30 边界处未对齐):
大多数循环大小现在都会受到 1 或 2 个循环的惩罚。当您考虑解码 16B 边界和每个周期解码 4 条指令时,1 个惩罚案例是有意义的,而 2 个周期惩罚案例发生在循环中,由于某种原因 DSB 用于循环中的 1 条指令(可能是 dec
指令出现在它自己的 32 字节块中),并且会产生一些 DSB<->MITE 切换惩罚。
在某些情况下,当它最终更好地对齐循环的结尾时,错位不会造成伤害。我测试了错位,它以相同的方式持续到 200 uop 循环。如果你从表面上看预解码器的描述,看起来,如上所述,它们应该能够隐藏一个用于未对齐的提取气泡,但它并没有发生(也许队列不够大)。
DSB(Uop 缓存)
uop 缓存(Intel 喜欢称其为 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 循环的图表。如果你眯着眼睛,你可以看到 sub-optimal 在 64-uops 之前的行为(当循环在 LSD 中时)。在那之后,它是一个直接的镜头,4 IPC 一直到 1,000 uops(大约 900 可能是由于我的盒子加载所致):
接下来我们看看小到足以放入 uop 缓存的循环的性能。
LSD(循环蒸汽检测器)
重要说明: 英特尔显然 禁用了 Skylake(SKL150 勘误表)和 Kaby Lake(KBL095、KBW095 勘误表)芯片上的 LSD由于 a bug 与超线程和 LSD 之间的交互相关,通过微代码更新和 Skylake-X 开箱即用。对于这些芯片,下图可能没有高达 64 微指令的有趣区域;相反,它看起来和64 uops 后的区域。
循环流检测器可以缓存最多 64 微指令的小循环(在 Skylake 上)。在英特尔最近的文档中,它更多地被定位为一种 power-saving 机制而不是一种性能特征 - 尽管肯定没有提到使用 LSD 的性能缺点。
运行 对于适合 LSD 的循环大小,我们得到以下 cycles/iteration 行为:
这里的红线是LSD传送的微指令的百分比。对于从 5 到 56 微指令的所有环路大小,它以 100% 的速度趋于平稳。
对于 3 和 4 微指令循环,我们有不寻常的行为,即分别有 16% 和 25% 的微指令是从传统解码器传送的。嗯?幸运的是,它似乎并没有影响循环吞吐量,因为这两种情况都达到了最大吞吐量 1 loop/cycle - 尽管人们可能会期待一些 MITE<->LSD t运行sition 惩罚。
循环大小在 57 和 62 微指令之间,从 LSD 传递的微指令数量表现出一些奇怪的行为——大约 70% 的微指令是从 LSD 传递的,其余的是从 DSB 传递的。 Skylake 名义上有一个 64-uop LSD,所以这是在超过 LSD 大小之前的某种 t运行sition - 也许在 IDQ 中有某种内部对齐(在其上实现了 LSD)在此阶段仅对 LSD 造成部分命中。这个阶段很短,performance-wise 似乎主要是它之前的 full-in-LSD 表现和它之后的 fully-in-DSB 表现的线性组合。
让我们看一下 5 到 56 微指令之间的结果主体。我们看到三个不同的区域:
从 3 微指令到 10 微指令的循环: 这里的行为很复杂。这是我们看到循环计数无法用单次循环迭代的静态行为解释的唯一区域8。 运行ge 足够短,很难说是否存在规律。 4、6 和 8 微指令的循环都以 N/4
周期最佳执行(这与下一个区域的模式相同)。
另一方面,10 微指令的循环在每次迭代中执行 2.66 个周期,这使其成为唯一在达到 34 微指令或更高的循环大小(除了异常值为 26)。这对应于 4, 4, 4, 3
的重复 uop/cycle 执行率。对于 5 微指令的循环,每次迭代得到 1.33 个周期,非常接近但与 1.25 的理想值不同。这对应于 4, 4, 4, 4, 3
.
的执行率
这些结果很难解释。结果可从 运行 运行 重复,并且对更改具有鲁棒性,例如将 nop 换成实际上执行类似 mov ecx, 123
的指令。这可能与每 2 个周期取 b运行ch 1 的限制有关,它适用于除 "very small" 之外的所有循环。可能是 uops 偶尔会排成一行,这样就会出现这种限制,从而导致额外的循环。一旦达到 12 微秒或以上,就不会发生这种情况,因为每次迭代总是至少需要三个周期。
从 11 到 32-uops 的循环: 我们看到一个 stair-step 模式,但周期为两个。基本上所有具有 even 个 uops 的循环都以最佳方式执行 - 即,正好需要 N/4
个循环。具有奇数 uops 的循环浪费一个 "issue slot",并且与具有更多 uops 的循环采用相同数量的周期(即,17 uop 循环与 18 uop 循环采用相同的 4.5 个周期)。因此,对于许多 uop 计数,我们的行为比 ceiling(N/4)
更好,并且我们有第一个证据表明 Skylake 至少可以在 non-integral 个周期内执行循环。
唯一的异常值是 N=25 和 N=26,它们都比预期的时间长了大约 1.5%。它很小但可重现,并且可以在文件中四处移动函数。这太小了,无法用 per-iteration 效应来解释,除非它有一个巨大的周期,所以它可能是别的原因。
此处的整体行为与硬件完全一致(除了 25/26 异常)展开循环 2 倍。
从 33 到 ~64 uops 的循环: 我们再次看到 stair-step 模式,但周期为 4,平均性能比 up-to 32 uop 大小写。行为完全是 ceiling(N/4)
- 也就是说,与传统解码器的情况相同。因此,对于 32 到 64 微指令的循环,LSD 与传统解码器相比没有明显优势,就前端吞吐量而言,针对此特定限制。当然,LSD 还有很多其他方面更好——它避免了很多为更复杂或更长的指令而发生的潜在解码瓶颈,并且它节省了功率等。
所有这些都非常令人惊讶,因为这意味着从 uop 缓存传递的循环通常在前端比从 LSD 传递的循环执行 更好,尽管 LSD 通常被定位为比 DSB 更好的 uops 来源(例如,作为建议的一部分,尽量保持循环足够小以适应 LSD)。
这是查看相同数据的另一种方式 - 根据给定 uop 计数的效率损失与每个周期 4 uops 的理论最大吞吐量。 10% 的效率命中意味着您只有 90% 的吞吐量,您可以通过简单的 N/4
公式计算出吞吐量。
此处的整体行为与硬件不进行任何展开一致,这是有道理的,因为在 64 微指令的缓冲区中根本无法展开超过 32 微指令的循环。
上面讨论的三个区域颜色不同,至少可以看到竞争效应:
在其他条件相同的情况下,微指令数越大,效率越低。命中是每次迭代只有一次的固定成本,因此较大的循环支付较小的 relative 成本。
当您进入 33+ uop 区域时,效率会大幅上升:吞吐量损失的大小都会增加,受影响的 uop 计数的数量也会翻倍。
第一个区域有点乱,7 uops是最差的整体uop数。
对齐
上面的 DSB 和 LSD 分析是针对与 32 字节边界对齐的循环条目,但未对齐的情况似乎在任何一种情况下都没有受到影响:与 material 没有区别对齐的情况(除了我没有进一步调查的小于 10 微指令的一些小变化)。
这是 32N-2
和 32N+2
的未对齐结果(即 32B 边界前后的循环顶部 2 个字节):
还显示了理想的 N/4
线以供参考。
哈斯韦尔
接下来看看之前的微架构:Haswell。此处的数字由用户 Iwillnotexist Idonotexist.
慷慨提供
LSD + 旧版解码管道
首先,来自 "dense code" 测试的结果,它测试 LSD(对于小 uop 计数)和遗留管道(对于更大的 uop 计数,因为 DSB 的循环 "busts out" 由于指令密度。
当每个架构从 LSD 为密集循环提供微指令时,我们立即看到在方面的差异。下面我们比较 Skylake 和 Haswell 的 dense 代码短循环(每条指令 1 个字节)。
如上所述,Skylake 循环在恰好 19 微指令时停止从 LSD 传送,正如预期的每 32 字节代码区域 18 微指令。另一方面,对于 16-uop 和 17-uop 循环,Haswell 似乎也停止从 LSD 可靠地传送。我对此没有任何解释。在 3 微指令的情况下也存在差异:奇怪的是,在 3 微指令和 4 微指令的情况下,两个处理器都只从 LSD 中提供 一些 微指令,但确切的数量是4 微指令相同,与 3 微指令不同。
虽然我们最关心的是实际表现,对吧?因此,让我们看一下 cycles/iteration 的 32 字节对齐 dense 代码案例:
这与上面显示的 Skylake 数据相同(已删除未对齐的系列),旁边绘制了 Haswell。您会立即注意到 Haswell 的模式 similar,但不相同。如上,这里有两个区域:
旧版解码
大于 ~16-18 微指令的循环(上面描述了不确定性)是从传统解码器传递的。 Haswell 的模式与 Skylake 有所不同。
对于 19-30 微秒的 运行ge,它们是相同的,但在那之后 Haswell 打破了模式。 Skylake 需要 ceil(N/4)
个周期来处理从旧解码器传送的循环。另一方面,Haswell 似乎采用 ceil((N+1)/4) + ceil((N+2)/12) - ceil((N+1)/12)
之类的东西。好的,这很混乱(更短的形式,有人吗?) - 但基本上这意味着虽然 Skylake 以最佳方式执行 4*N 循环的循环(即,在 4-uops/cycle 处),但此类循环(在本地)通常是 least optimal count (至少在本地) - 执行这样的循环比Skylake多一个周期。所以你实际上最好在 Haswell 上使用 4N-1 微指令的循环,除了,这种循环的 25% 是 also 的形式16-1N(31、47、63 等)需要一个额外的周期。它开始听起来像是闰年计算 - 但这种模式可能在上面的视觉上是最好理解的。
我不认为这种模式是固有的 uop dispatch on 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),以及解码的周期。规则基本上是ext 4 指令被解码,只要它们落在当前的 16B 块中。否则他们必须等到下一个周期。对于 N=35,我们看到在周期 4 中丢失了 1 个解码槽(16B 块中只剩下 3 条指令),但除此之外,循环与 16B 边界甚至最后一个周期( 9) 可以解码4条指令。
这是对 N=36 的 t运行 分类查看,除了循环结束之外,它是相同的:
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>
现在在第 3 个也是最后一个 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 nop 的块解码 5-5-5-1,而不是 4-4-在 Haswell 上为 4-4,因此您只能在边缘获得好处:例如,在上面 N=36 的情况下,Skylake 可以解码所有剩余的 5 条指令,而 Haswell 为 4-1,节省了一个周期。
结果是,遗留解码器行为似乎可以以相当直接的方式理解,主要优化建议是继续修改代码,使其落入 "smartly" 16B对齐的块(也许 NP-hard 就像装箱一样?)。
DSB(又是 LSD)
接下来让我们看一下在 LSD 或 DSB 之外提供代码的场景 - 通过使用 "long nop" 测试避免打破每 32B 块限制 18-uop,因此保持在DSB.
Haswell vs Skylake:
注意 LSD 行为 - 此处 Haswell 恰好在 57 uops 时停止服务于 LSD,这与发布的 57 uops LSD 大小完全一致。没有像我们在 Skylake 上看到的那样奇怪的 "transition period"。 Haswell 对于 3 和 4 微指令也有奇怪的行为,其中分别只有 ~0% 和 ~40% 的微指令来自 LSD。
Performance-wise,Haswell 通常 in-line 与 Skylake 有一些偏差,例如,大约 65、77 和 97 微指令,它四舍五入到下一个周期,而 Skylake 总是能够维持 4 uops/cycle,即使这会导致 non-integer 循环次数。与预期的 25 和 26 微指令的轻微偏差已经消失。也许 Skylake 的 6-uop 交付率有助于它避免 uop-cache Haswell 在其 4-uop 交付率下遇到的对齐问题。
其他架构
以下附加架构的结果由用户 Andreas Abel 友情提供,但由于此处已达到字符数限制,我们将不得不使用其他答案进行进一步分析。
需要帮助
虽然许多平台的结果已由社区友善地提供,但我仍然对比 Nehalem 更早、比 Coffee Lake 更新的芯片(特别是 Cannon Lake,这是一个新的 uarch)的结果感兴趣。在 GitHub 中以 .ods
格式生成这些结果 is public. Also, the results above are available 的代码也是如此。
0 特别是,在 Skylake 中,传统解码器的最大吞吐量明显从 4 微指令增加到 5 微指令,而微指令缓存的最大吞吐量从 4 微指令增加到 6 微指令。两者这些可能会影响此处描述的结果。
1 英特尔实际上喜欢将传统解码器称为 MITE(Micro-instruction T运行slation 引擎),也许是因为它是 faux-pas 用 legacy 内涵实际标记架构的任何部分。
2 从技术上讲,还有另一个更慢的 uops 来源 - MS(微码排序引擎),用于实现任何超过 4 uops 的指令,但是我们在这里忽略它,因为 none 我们的循环包含微代码指令。
3 这是有效的,因为任何对齐的 32 字节块在其 uop 缓存槽中最多可以使用 3 路,并且每个槽最多容纳 6 uops。所以如果你在一个32B的chunk中使用了超过3 * 6 = 18
个uops,代码根本就无法存入uop缓存中。在实践中可能很少遇到这种情况,因为 t代码需要非常密集(每条指令少于 2 个字节)才能触发此操作。
4 nop
指令解码为一个 uop,但在执行之前不会被消除(即,它们不使用执行端口)-但仍然在前端占用 space,因此计算我们感兴趣的各种限制。
5 LSD是loop stream detector,直接在IDQ中缓存最多64(Skylake)uops的小循环.在较早的架构上,它可以保持 28 微指令(两个逻辑核心都处于活动状态)或 56 微指令(一个逻辑核心处于活动状态)。
6 我们不能轻易地在这个模式中安装一个 2 uop 循环,因为那意味着零个 nop
指令,意味着 dec
和jnz
指令会 macro-fuse,uop 计数也会发生相应的变化。相信我的话,所有 4 微指令或更少微指令的循环最多执行 1 cycle/iteration.
7 为了好玩,我只是 运行 perf stat
对着一个简短的 运行 Firefox 我打开了一个标签并点击了一些 Stack Overflow 问题。对于交付的指令,我从 DSB 获得了 46%,从传统解码器获得了 50%,从 LSD 获得了 4%。这表明至少对于像浏览器这样的大型 b运行chy 代码,DSB 仍然无法捕获大部分代码(幸运的是,传统解码器还不错)。
8 通过这个,我的意思是所有其他循环计数都可以通过简单地通过以 uops 为单位的 "effective" 积分循环成本来解释(这可能更高比实际大小是 uops)并除以 4。对于这些非常短的循环,这不起作用 - 通过将任何整数除以 4,您无法获得每次迭代 1.333 个循环。换句话说,在所有其他区域对于某个整数 N,成本具有 N/4 的形式。
9 事实上我们知道 Skylake 可以 从传统解码器每个周期传送 5 微码,但我们不知道是否这 5 条微指令可以来自 5 条不同的指令,或者只有 4 条或更少。也就是说,我们期望 Skylake 可以解码 2-1-1-1
模式,但我不确定它是否可以解码 1-1-1-1-1
模式。上面的结果给出了一些证据,证明它确实可以解码1-1-1-1-1
.
这是原始答案的后续,根据 Andreas Abel:
提供的测试结果分析另外五个架构的行为
- 尼哈勒姆
- 沙桥
- 常春藤桥
- 布罗德韦尔
- 咖啡湖
除了 Skylake 和 Haswell 之外,我们还快速查看了这些架构的结果。它只需要是一个 "quick" 外观,因为除了 Nehalem 之外的所有架构都遵循上面讨论的现有模式之一。
首先,短 nop 情况,它使用传统解码器(对于不适合 LSD 的循环)和 LSD。这是此场景的 cycles/iteration,适用于所有 7 种架构。
图 2.1:所有架构的密集 nop 性能:
这张图真的很忙(点击查看大图)并且有点难以阅读,因为许多架构的结果都在彼此之上,但我试图确保一个专用的 reader 可以跟踪任何架构的线路。
首先,让我们讨论一下大异常值:Nehalem。所有其他架构的斜率大致遵循 4 uops/cycle 线,但 Nehalem 几乎正好是每周期 3 微指令,因此很快落后于所有其他架构。在初始 LSD 区域之外,该线也完全平滑,没有其他架构中看到的 "stair step" 外观。
这与 Nehalem 的 uop 退休 限制为 3 uops/cycle 完全一致。这是 LSD 之外的 uops 的瓶颈:它们每个周期都以大约 3 uops 的速度执行,在退役时成为瓶颈。前端不是瓶颈,所以确切的 uop 计数和解码安排并不重要,因此没有阶梯。
除了 Nehalem 之外,其他架构,除了 Broadwell 之外,都相当清楚地分为几组:类 Haswell 或类 Skylake。也就是说,所有 Sandy Bridge、Ivy Bridge 和 Haswell 的行为都像 Haswell,循环大于 15 微秒(Haswell 行为在另一个答案中讨论)。尽管它们是不同的微架构,但它们的行为在很大程度上是相同的,因为它们的传统解码功能是相同的。在大约 15 微指令以下,我们看到 Haswell 对于任何不是 4 的倍数的微指令都更快一些。也许由于更大的 LSD,它在 LSD 中得到了额外的展开,或者还有其他 "small loop" 优化。对于 Sandy Bridge 和 Ivy Bridge,这意味着小循环绝对应该以 4 的倍数为目标。
Coffee Lake 的行为类似于 Skylake1。这是有道理的,因为微架构是相同的。在大约 16 微秒以下,Coffee Lake 似乎比 Skylake 更好,但这只是 Coffee Lake 默认禁用 LSD 的效果。 Skylake 在启用 LSD 的情况下进行了测试,之后英特尔由于安全问题通过微代码更新将其禁用。 Coffee Lake 在已知此问题后发布,因此开箱即用地禁用了 LSD。因此,对于此测试,Coffee Lake 使用 DSB(对于低于约 18 微指令的循环,仍然可以放入 DSB)或传统解码器(对于循环的其余部分),这会在小微指令数量下获得更好的结果LSD 施加开销的循环(有趣的是,对于较大的循环,LSD 和传统解码器恰好施加完全相同的开销,原因完全不同)。
最后,我们看一下 2 字节 NOP,它们的密度不足以阻止使用 DSB(因此这种情况更能反映典型代码)。
图 2.1:2 字节 nop 性能:
同样,结果与之前的图表相同。 Nehalem 仍然是每个周期 3 微指令的异常瓶颈。对于高达 60 左右 uops 的范围,除 Coffee Lake 之外的所有架构都在使用 LSD,我们看到 Sandy Bridge 和 Ivy Bridge 在这里的表现有点差,四舍五入到下一个周期,因此只能实现最大吞吐量 4 uops/cycle 如果循环中的微指令数是 4 的倍数。超过 32 微指令时,Haswell 和新的 uarchs 的 "unrolling" 特性没有任何效果,所以一切大致相同。
Sandy Bridge 实际上有几个微指令范围(例如,从 36 到 44 微指令),在这些范围内它的性能优于较新的架构。这似乎是因为并非所有环路都被 LSD 检测到,并且在这些范围内,环路由 DSB 提供服务。由于 DSB 通常更快,因此在这些情况下 Sandy Bridge 也更快。
英特尔的说法
您实际上可以在英特尔优化手册第 3.4.2.5 节中找到专门处理此主题的部分,正如 Andreas Abel 在评论中指出的那样。在那里,英特尔说:
The LSD holds micro-ops that construct small “infinite” loops.
Micro-ops from the LSD are allocated in the out-of-order engine. The
loop in the LSD ends with a taken branch to the beginning of the loop.
The taken branch at the end of the loop is always the last micro-op
allocated in the cycle. The instruction at the beginning of the loop
is always allocated at the next cycle. If the code performance is
bound by front end bandwidth, unused allocation slots result in a
bubble in allocation, and can cause performance degrada- tion.
Allocation bandwidth in Intel microarchitecture code name Sandy Bridge
is four micro-ops per cycle. Performance is best, when the number of
micro-ops in the LSD result in the least number of unused allo- cation
slots. You can use loop unrolling to control the number of micro-ops
that are in the LSD.
他们继续展示了一个示例,其中由于 LSD "rounding",将循环展开两倍对性能没有帮助,但展开三倍有效。该示例非常令人困惑,因为它实际上混合了两种效果,因为展开更多还减少了循环开销,因此减少了每次迭代的微指令数。一个更有趣的例子是,由于 LSD 舍入效应,展开循环 更少 次导致性能提高。
这部分似乎准确地描述了 Sandy Bridge 和 Ivy Bridge 中的行为。上面的结果表明这两种架构都按照描述的方式工作,并且分别为 4N+3、4N+2 或 4N+1 微指令的循环损失了 1、2 或 3 微指令执行槽。
Haswell 及更高版本的新性能尚未更新。如另一个答案中所述,性能比上述简单模型有所提高,并且行为更加复杂。
1 在 16 微秒处有一个奇怪的异常值,其中 Coffee Lake 的性能比所有其他架构都要差,甚至 Nehalem(回归大约 50%),但也许这个测量噪音?
TL;DR: 对于正好由 7 微指令组成的紧密循环,它会导致退休带宽利用率低下。考虑手动展开循环,因此循环将包含 12 uops
我最近面临着由 7 微指令组成的循环的退休带宽退化。在我自己做了一些研究之后,快速谷歌搜索将我带到了这个话题。这是我申请 Kaby Lake i7-8550U 的 2 美分 CPU:
正如@BeeOnRope 指出的那样,LSD 在 KbL i7-8550U
.
等芯片上被关闭
考虑以下 NASM 宏
;rdi = 1L << 31
%macro nops 1
align 32:
%%loop:
times %1 nop
dec rdi
ja %%loop
%endmacro
下面是 "average retirement rate" 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 uops/c。当 IDQ 溢出时,uops 缓存保持空闲状态,直到它可以再次传送 6 uops。
为了检查 uops 缓存如何保持空闲,让我们比较一下 idq.all_dsb_cycles_any_uops
和周期
传递给idq的微指令周期数等于7微指令循环的总周期数。相比之下,6 微指令循环的计数器明显不同。
要检查的关键计数器是 idq_uops_not_delivered.*
从 7 微指令的循环可以看出,重命名器采用 4|3 组,这导致退休带宽利用率低下。
我想知道各种大小的循环在最近的 x86 处理器上如何执行,作为 uops 数的函数。
引用 Peter Cordes 的话,他在 another question 中提出了非 4 的倍数计数问题:
I also found that the uop bandwidth out of the loop buffer isn't a constant 4 per cycle, if the loop isn't a multiple of 4 uops. (i.e. it's abc, abc, ...; not abca, bcab, ...). Agner Fog's microarch doc unfortunately wasn't clear on this limitation of the loop buffer.
问题是关于循环是否需要是 N 微指令的倍数才能以最大微指令吞吐量执行,其中 N 是处理器的宽度。 (即,最近的 Intel 处理器为 4)。在谈论 "width" 和计数 uops 时有很多复杂的因素,但我主要想忽略这些。特别是,假设没有微观或宏观融合。
Peter 给出了以下循环体中包含 7 微指令的循环示例:
A 7-uop loop will issue groups of 4|3|4|3|... I haven't tested larger loops (that don't fit in the loop buffer) to see if it's possible for the first instruction from the next iteration to issue in the same group as the taken-branch to it, but I assume not.
更一般地说,声明的是在其主体中具有 x
微指令的循环的每次迭代将至少进行 ceil(x / 4)
次迭代,而不是简单地 x / 4
。
部分或所有最新的 x86 兼容处理器是否如此?
我用 Linux perf
做了一些调查,以帮助在我的 Skylake i7-6700HQ 盒子上回答这个问题,另一位用户友情地提供了 Haswell 结果。下面的分析适用于 Skylake,但随后与 Haswell 进行了比较。
其他架构可能会有所不同0,为了帮助解决所有问题,我欢迎提供更多结果。 source is available).
这个问题主要涉及前端,因为在最近的架构中,前端强加了每个周期四个 fused-domain 微指令的硬限制。
循环性能规则总结
首先,我将根据一些 "performance rules" 来总结结果,以便在处理小循环时牢记。还有很多其他性能规则 - 这些是它们的补充(即,您可能不会为了满足这些规则而违反另一条规则)。这些规则最直接适用于 Haswell 和后来的架构 - 请参阅
首先,计算循环中 macro-fused 微指令的数量。您可以使用 Agner 的 instruction tables 直接查找每条指令,除了 ALU uop 和紧随其后的 b运行ch 通常会融合在一起成为一个 uop。然后根据这个计数:
- 如果计数是 4 的倍数,那很好:这些循环执行得最好。
- 如果计数是偶数且小于 32,那很好,除非是 10,在这种情况下,如果可以的话,您应该展开到另一个偶数。
- 对于奇数,如果可以的话,您应该尝试展开到小于 32 或 4 的倍数的偶数。
- 对于大于 32 微指令但小于 64 的循环,如果它不是 4 的倍数,您可能想要展开:超过 64 微指令,您将在 Sklyake 上获得任何值的高效性能,几乎Haswell 上的所有值(有一些偏差,可能与对齐有关)。这些循环的低效率仍然相对较小:最应避免的值是
4N + 1
计数,其次是4N + 2
计数。
调查结果摘要
对于由 uop 缓存提供的代码,没有明显的 multiple-of-4 效果。可以以每周期 4 fused-domain 微指令的吞吐量执行任意数量微指令的循环。
对于传统解码器处理的代码,情况恰恰相反:循环执行时间被限制为循环的整数倍,因此不是 4 微指令倍数的循环无法达到 4 uops/cycle,如他们浪费了一些 issue/execution 个位置。
对于从循环流检测器 (LSD) 发出的代码,情况是两者的混合,下面将对此进行更详细的解释。通常,小于 32 微指令和偶数微指令的循环最佳执行,而 odd-sized 循环则不然,更大的循环需要 multiple-of-4 微指令才能最佳执行。
英特尔的说法
英特尔实际上在他们的优化手册中对此有说明,其他答案中有详细说明。
详情
任何人 well-versed 最近的 x86-64 架构都知道,在任何时候,前端的提取和解码部分都可能以一种不同的模式工作,具体取决于代码大小和其他因素。事实证明,这些不同的模式在循环大小调整方面都有不同的行为。我将在后面分别介绍它们。
旧版解码器
legacy decoder1是使用的完整machine-code-to-uops解码器2 当代码不适合 uop 缓存机制(LSD 或 DSB)时。发生这种情况的主要原因是代码工作集大于 uop 缓存(在理想情况下约为 1500 uops,在实践中更少)。不过,对于此测试,我们将利用以下事实:如果对齐的 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 nop
s,加上两个循环控制指令)。没有 macro-fusion,因为我们总是将 dec
和 jnz
与至少一个 nop
分开,也没有 micro-fusion。最后,在(隐含的 icache 访问之外)没有内存访问。
请注意,此循环非常密集 - 每条指令约 1 个字节(因为 nop
指令每个为 1 个字节) - 因此我们将触发 > 32B 块条件下的 18 条指令一旦在循环中命中 19 条指令。基于检查 perf
性能计数器 lsd.uops
和 idq.mite_uops
这正是我们所看到的:基本上 100% 的指令来自 LSD 直到并包括 18 微指令循环,但在 19 微指令及以上,100% 来自传统解码器。
无论如何,这是从 3 到 99 uops 的所有循环大小的 cycles/iteration6:
蓝点是适合 LSD 的循环,表现出一些复杂的行为。我们稍后再看这些。
红点(从 19 uops/iteration 开始)由传统解码器处理,显示出非常可预测的模式:
- 所有具有
N
微指令的循环正好进行ceiling(N/4)
次迭代
因此,至少对于传统解码器而言,Peter 的观察在 Skylake 上完全成立:4 uops 的倍数 的循环可能以 4 的 IPC 执行,但任何其他数字of uops 将浪费 1、2 或 3 个执行槽(分别用于具有 4N+3
、4N+2
、4N+1
指令的循环)。
我不清楚为什么会这样。虽然如果你考虑解码发生在连续的 16B 块中,这似乎很明显,因此在 4 uops/cycle 循环的解码速率下,不是 4 的倍数总是在循环中有一些尾随(浪费的)槽 jnz
遇到指令。然而,实际的提取和解码单元是由预解码和解码阶段组成的,有一个队列in-between。预解码阶段实际上具有 6 条指令的吞吐量,但每个周期仅解码到 16 字节边界的末尾。这似乎意味着循环结束时出现的气泡可以被预解码器吸收 -> 解码队列,因为预解码器的平均吞吐量高于 4.
所以我无法根据我对预解码器工作原理的理解来完全解释这一点。可能是在解码或 pre-decoding 中有一些额外的限制阻止了 non-integral 循环计数。例如,即使跳转后的指令在预解码队列中可用,遗留解码器也可能无法解码跳转两侧的指令。可能跟需要handle macro-fusion.
有关上面的测试显示了循环顶部在 32 字节边界上对齐的行为。下面是同一张图,但添加了一个系列,显示了循环顶部向上移动 2 个字节时的效果(即,现在在 32N + 30 边界处未对齐):
大多数循环大小现在都会受到 1 或 2 个循环的惩罚。当您考虑解码 16B 边界和每个周期解码 4 条指令时,1 个惩罚案例是有意义的,而 2 个周期惩罚案例发生在循环中,由于某种原因 DSB 用于循环中的 1 条指令(可能是 dec
指令出现在它自己的 32 字节块中),并且会产生一些 DSB<->MITE 切换惩罚。
在某些情况下,当它最终更好地对齐循环的结尾时,错位不会造成伤害。我测试了错位,它以相同的方式持续到 200 uop 循环。如果你从表面上看预解码器的描述,看起来,如上所述,它们应该能够隐藏一个用于未对齐的提取气泡,但它并没有发生(也许队列不够大)。
DSB(Uop 缓存)
uop 缓存(Intel 喜欢称其为 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 循环的图表。如果你眯着眼睛,你可以看到 sub-optimal 在 64-uops 之前的行为(当循环在 LSD 中时)。在那之后,它是一个直接的镜头,4 IPC 一直到 1,000 uops(大约 900 可能是由于我的盒子加载所致):
接下来我们看看小到足以放入 uop 缓存的循环的性能。
LSD(循环蒸汽检测器)
重要说明: 英特尔显然 禁用了 Skylake(SKL150 勘误表)和 Kaby Lake(KBL095、KBW095 勘误表)芯片上的 LSD由于 a bug 与超线程和 LSD 之间的交互相关,通过微代码更新和 Skylake-X 开箱即用。对于这些芯片,下图可能没有高达 64 微指令的有趣区域;相反,它看起来和64 uops 后的区域。
循环流检测器可以缓存最多 64 微指令的小循环(在 Skylake 上)。在英特尔最近的文档中,它更多地被定位为一种 power-saving 机制而不是一种性能特征 - 尽管肯定没有提到使用 LSD 的性能缺点。
运行 对于适合 LSD 的循环大小,我们得到以下 cycles/iteration 行为:
这里的红线是LSD传送的微指令的百分比。对于从 5 到 56 微指令的所有环路大小,它以 100% 的速度趋于平稳。
对于 3 和 4 微指令循环,我们有不寻常的行为,即分别有 16% 和 25% 的微指令是从传统解码器传送的。嗯?幸运的是,它似乎并没有影响循环吞吐量,因为这两种情况都达到了最大吞吐量 1 loop/cycle - 尽管人们可能会期待一些 MITE<->LSD t运行sition 惩罚。
循环大小在 57 和 62 微指令之间,从 LSD 传递的微指令数量表现出一些奇怪的行为——大约 70% 的微指令是从 LSD 传递的,其余的是从 DSB 传递的。 Skylake 名义上有一个 64-uop LSD,所以这是在超过 LSD 大小之前的某种 t运行sition - 也许在 IDQ 中有某种内部对齐(在其上实现了 LSD)在此阶段仅对 LSD 造成部分命中。这个阶段很短,performance-wise 似乎主要是它之前的 full-in-LSD 表现和它之后的 fully-in-DSB 表现的线性组合。
让我们看一下 5 到 56 微指令之间的结果主体。我们看到三个不同的区域:
从 3 微指令到 10 微指令的循环: 这里的行为很复杂。这是我们看到循环计数无法用单次循环迭代的静态行为解释的唯一区域8。 运行ge 足够短,很难说是否存在规律。 4、6 和 8 微指令的循环都以 N/4
周期最佳执行(这与下一个区域的模式相同)。
另一方面,10 微指令的循环在每次迭代中执行 2.66 个周期,这使其成为唯一在达到 34 微指令或更高的循环大小(除了异常值为 26)。这对应于 4, 4, 4, 3
的重复 uop/cycle 执行率。对于 5 微指令的循环,每次迭代得到 1.33 个周期,非常接近但与 1.25 的理想值不同。这对应于 4, 4, 4, 4, 3
.
这些结果很难解释。结果可从 运行 运行 重复,并且对更改具有鲁棒性,例如将 nop 换成实际上执行类似 mov ecx, 123
的指令。这可能与每 2 个周期取 b运行ch 1 的限制有关,它适用于除 "very small" 之外的所有循环。可能是 uops 偶尔会排成一行,这样就会出现这种限制,从而导致额外的循环。一旦达到 12 微秒或以上,就不会发生这种情况,因为每次迭代总是至少需要三个周期。
从 11 到 32-uops 的循环: 我们看到一个 stair-step 模式,但周期为两个。基本上所有具有 even 个 uops 的循环都以最佳方式执行 - 即,正好需要 N/4
个循环。具有奇数 uops 的循环浪费一个 "issue slot",并且与具有更多 uops 的循环采用相同数量的周期(即,17 uop 循环与 18 uop 循环采用相同的 4.5 个周期)。因此,对于许多 uop 计数,我们的行为比 ceiling(N/4)
更好,并且我们有第一个证据表明 Skylake 至少可以在 non-integral 个周期内执行循环。
唯一的异常值是 N=25 和 N=26,它们都比预期的时间长了大约 1.5%。它很小但可重现,并且可以在文件中四处移动函数。这太小了,无法用 per-iteration 效应来解释,除非它有一个巨大的周期,所以它可能是别的原因。
此处的整体行为与硬件完全一致(除了 25/26 异常)展开循环 2 倍。
从 33 到 ~64 uops 的循环: 我们再次看到 stair-step 模式,但周期为 4,平均性能比 up-to 32 uop 大小写。行为完全是 ceiling(N/4)
- 也就是说,与传统解码器的情况相同。因此,对于 32 到 64 微指令的循环,LSD 与传统解码器相比没有明显优势,就前端吞吐量而言,针对此特定限制。当然,LSD 还有很多其他方面更好——它避免了很多为更复杂或更长的指令而发生的潜在解码瓶颈,并且它节省了功率等。
所有这些都非常令人惊讶,因为这意味着从 uop 缓存传递的循环通常在前端比从 LSD 传递的循环执行 更好,尽管 LSD 通常被定位为比 DSB 更好的 uops 来源(例如,作为建议的一部分,尽量保持循环足够小以适应 LSD)。
这是查看相同数据的另一种方式 - 根据给定 uop 计数的效率损失与每个周期 4 uops 的理论最大吞吐量。 10% 的效率命中意味着您只有 90% 的吞吐量,您可以通过简单的 N/4
公式计算出吞吐量。
此处的整体行为与硬件不进行任何展开一致,这是有道理的,因为在 64 微指令的缓冲区中根本无法展开超过 32 微指令的循环。
上面讨论的三个区域颜色不同,至少可以看到竞争效应:
在其他条件相同的情况下,微指令数越大,效率越低。命中是每次迭代只有一次的固定成本,因此较大的循环支付较小的 relative 成本。
当您进入 33+ uop 区域时,效率会大幅上升:吞吐量损失的大小都会增加,受影响的 uop 计数的数量也会翻倍。
第一个区域有点乱,7 uops是最差的整体uop数。
对齐
上面的 DSB 和 LSD 分析是针对与 32 字节边界对齐的循环条目,但未对齐的情况似乎在任何一种情况下都没有受到影响:与 material 没有区别对齐的情况(除了我没有进一步调查的小于 10 微指令的一些小变化)。
这是 32N-2
和 32N+2
的未对齐结果(即 32B 边界前后的循环顶部 2 个字节):
还显示了理想的 N/4
线以供参考。
哈斯韦尔
接下来看看之前的微架构:Haswell。此处的数字由用户 Iwillnotexist Idonotexist.
慷慨提供LSD + 旧版解码管道
首先,来自 "dense code" 测试的结果,它测试 LSD(对于小 uop 计数)和遗留管道(对于更大的 uop 计数,因为 DSB 的循环 "busts out" 由于指令密度。
当每个架构从 LSD 为密集循环提供微指令时,我们立即看到在方面的差异。下面我们比较 Skylake 和 Haswell 的 dense 代码短循环(每条指令 1 个字节)。
如上所述,Skylake 循环在恰好 19 微指令时停止从 LSD 传送,正如预期的每 32 字节代码区域 18 微指令。另一方面,对于 16-uop 和 17-uop 循环,Haswell 似乎也停止从 LSD 可靠地传送。我对此没有任何解释。在 3 微指令的情况下也存在差异:奇怪的是,在 3 微指令和 4 微指令的情况下,两个处理器都只从 LSD 中提供 一些 微指令,但确切的数量是4 微指令相同,与 3 微指令不同。
虽然我们最关心的是实际表现,对吧?因此,让我们看一下 cycles/iteration 的 32 字节对齐 dense 代码案例:
这与上面显示的 Skylake 数据相同(已删除未对齐的系列),旁边绘制了 Haswell。您会立即注意到 Haswell 的模式 similar,但不相同。如上,这里有两个区域:
旧版解码
大于 ~16-18 微指令的循环(上面描述了不确定性)是从传统解码器传递的。 Haswell 的模式与 Skylake 有所不同。
对于 19-30 微秒的 运行ge,它们是相同的,但在那之后 Haswell 打破了模式。 Skylake 需要 ceil(N/4)
个周期来处理从旧解码器传送的循环。另一方面,Haswell 似乎采用 ceil((N+1)/4) + ceil((N+2)/12) - ceil((N+1)/12)
之类的东西。好的,这很混乱(更短的形式,有人吗?) - 但基本上这意味着虽然 Skylake 以最佳方式执行 4*N 循环的循环(即,在 4-uops/cycle 处),但此类循环(在本地)通常是 least optimal count (至少在本地) - 执行这样的循环比Skylake多一个周期。所以你实际上最好在 Haswell 上使用 4N-1 微指令的循环,除了,这种循环的 25% 是 also 的形式16-1N(31、47、63 等)需要一个额外的周期。它开始听起来像是闰年计算 - 但这种模式可能在上面的视觉上是最好理解的。
我不认为这种模式是固有的 uop dispatch on 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),以及解码的周期。规则基本上是ext 4 指令被解码,只要它们落在当前的 16B 块中。否则他们必须等到下一个周期。对于 N=35,我们看到在周期 4 中丢失了 1 个解码槽(16B 块中只剩下 3 条指令),但除此之外,循环与 16B 边界甚至最后一个周期( 9) 可以解码4条指令。
这是对 N=36 的 t运行 分类查看,除了循环结束之外,它是相同的:
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>
现在在第 3 个也是最后一个 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 nop 的块解码 5-5-5-1,而不是 4-4-在 Haswell 上为 4-4,因此您只能在边缘获得好处:例如,在上面 N=36 的情况下,Skylake 可以解码所有剩余的 5 条指令,而 Haswell 为 4-1,节省了一个周期。
结果是,遗留解码器行为似乎可以以相当直接的方式理解,主要优化建议是继续修改代码,使其落入 "smartly" 16B对齐的块(也许 NP-hard 就像装箱一样?)。
DSB(又是 LSD)
接下来让我们看一下在 LSD 或 DSB 之外提供代码的场景 - 通过使用 "long nop" 测试避免打破每 32B 块限制 18-uop,因此保持在DSB.
Haswell vs Skylake:
注意 LSD 行为 - 此处 Haswell 恰好在 57 uops 时停止服务于 LSD,这与发布的 57 uops LSD 大小完全一致。没有像我们在 Skylake 上看到的那样奇怪的 "transition period"。 Haswell 对于 3 和 4 微指令也有奇怪的行为,其中分别只有 ~0% 和 ~40% 的微指令来自 LSD。
Performance-wise,Haswell 通常 in-line 与 Skylake 有一些偏差,例如,大约 65、77 和 97 微指令,它四舍五入到下一个周期,而 Skylake 总是能够维持 4 uops/cycle,即使这会导致 non-integer 循环次数。与预期的 25 和 26 微指令的轻微偏差已经消失。也许 Skylake 的 6-uop 交付率有助于它避免 uop-cache Haswell 在其 4-uop 交付率下遇到的对齐问题。
其他架构
以下附加架构的结果由用户 Andreas Abel 友情提供,但由于此处已达到字符数限制,我们将不得不使用其他答案进行进一步分析。
需要帮助
虽然许多平台的结果已由社区友善地提供,但我仍然对比 Nehalem 更早、比 Coffee Lake 更新的芯片(特别是 Cannon Lake,这是一个新的 uarch)的结果感兴趣。在 GitHub 中以 .ods
格式生成这些结果 is public. Also, the results above are available 的代码也是如此。
0 特别是,在 Skylake 中,传统解码器的最大吞吐量明显从 4 微指令增加到 5 微指令,而微指令缓存的最大吞吐量从 4 微指令增加到 6 微指令。两者这些可能会影响此处描述的结果。
1 英特尔实际上喜欢将传统解码器称为 MITE(Micro-instruction T运行slation 引擎),也许是因为它是 faux-pas 用 legacy 内涵实际标记架构的任何部分。
2 从技术上讲,还有另一个更慢的 uops 来源 - MS(微码排序引擎),用于实现任何超过 4 uops 的指令,但是我们在这里忽略它,因为 none 我们的循环包含微代码指令。
3 这是有效的,因为任何对齐的 32 字节块在其 uop 缓存槽中最多可以使用 3 路,并且每个槽最多容纳 6 uops。所以如果你在一个32B的chunk中使用了超过3 * 6 = 18
个uops,代码根本就无法存入uop缓存中。在实践中可能很少遇到这种情况,因为 t代码需要非常密集(每条指令少于 2 个字节)才能触发此操作。
4 nop
指令解码为一个 uop,但在执行之前不会被消除(即,它们不使用执行端口)-但仍然在前端占用 space,因此计算我们感兴趣的各种限制。
5 LSD是loop stream detector,直接在IDQ中缓存最多64(Skylake)uops的小循环.在较早的架构上,它可以保持 28 微指令(两个逻辑核心都处于活动状态)或 56 微指令(一个逻辑核心处于活动状态)。
6 我们不能轻易地在这个模式中安装一个 2 uop 循环,因为那意味着零个 nop
指令,意味着 dec
和jnz
指令会 macro-fuse,uop 计数也会发生相应的变化。相信我的话,所有 4 微指令或更少微指令的循环最多执行 1 cycle/iteration.
7 为了好玩,我只是 运行 perf stat
对着一个简短的 运行 Firefox 我打开了一个标签并点击了一些 Stack Overflow 问题。对于交付的指令,我从 DSB 获得了 46%,从传统解码器获得了 50%,从 LSD 获得了 4%。这表明至少对于像浏览器这样的大型 b运行chy 代码,DSB 仍然无法捕获大部分代码(幸运的是,传统解码器还不错)。
8 通过这个,我的意思是所有其他循环计数都可以通过简单地通过以 uops 为单位的 "effective" 积分循环成本来解释(这可能更高比实际大小是 uops)并除以 4。对于这些非常短的循环,这不起作用 - 通过将任何整数除以 4,您无法获得每次迭代 1.333 个循环。换句话说,在所有其他区域对于某个整数 N,成本具有 N/4 的形式。
9 事实上我们知道 Skylake 可以 从传统解码器每个周期传送 5 微码,但我们不知道是否这 5 条微指令可以来自 5 条不同的指令,或者只有 4 条或更少。也就是说,我们期望 Skylake 可以解码 2-1-1-1
模式,但我不确定它是否可以解码 1-1-1-1-1
模式。上面的结果给出了一些证据,证明它确实可以解码1-1-1-1-1
.
这是原始答案的后续,根据 Andreas Abel:
提供的测试结果分析另外五个架构的行为- 尼哈勒姆
- 沙桥
- 常春藤桥
- 布罗德韦尔
- 咖啡湖
除了 Skylake 和 Haswell 之外,我们还快速查看了这些架构的结果。它只需要是一个 "quick" 外观,因为除了 Nehalem 之外的所有架构都遵循上面讨论的现有模式之一。
首先,短 nop 情况,它使用传统解码器(对于不适合 LSD 的循环)和 LSD。这是此场景的 cycles/iteration,适用于所有 7 种架构。
图 2.1:所有架构的密集 nop 性能:
这张图真的很忙(点击查看大图)并且有点难以阅读,因为许多架构的结果都在彼此之上,但我试图确保一个专用的 reader 可以跟踪任何架构的线路。
首先,让我们讨论一下大异常值:Nehalem。所有其他架构的斜率大致遵循 4 uops/cycle 线,但 Nehalem 几乎正好是每周期 3 微指令,因此很快落后于所有其他架构。在初始 LSD 区域之外,该线也完全平滑,没有其他架构中看到的 "stair step" 外观。
这与 Nehalem 的 uop 退休 限制为 3 uops/cycle 完全一致。这是 LSD 之外的 uops 的瓶颈:它们每个周期都以大约 3 uops 的速度执行,在退役时成为瓶颈。前端不是瓶颈,所以确切的 uop 计数和解码安排并不重要,因此没有阶梯。
除了 Nehalem 之外,其他架构,除了 Broadwell 之外,都相当清楚地分为几组:类 Haswell 或类 Skylake。也就是说,所有 Sandy Bridge、Ivy Bridge 和 Haswell 的行为都像 Haswell,循环大于 15 微秒(Haswell 行为在另一个答案中讨论)。尽管它们是不同的微架构,但它们的行为在很大程度上是相同的,因为它们的传统解码功能是相同的。在大约 15 微指令以下,我们看到 Haswell 对于任何不是 4 的倍数的微指令都更快一些。也许由于更大的 LSD,它在 LSD 中得到了额外的展开,或者还有其他 "small loop" 优化。对于 Sandy Bridge 和 Ivy Bridge,这意味着小循环绝对应该以 4 的倍数为目标。
Coffee Lake 的行为类似于 Skylake1。这是有道理的,因为微架构是相同的。在大约 16 微秒以下,Coffee Lake 似乎比 Skylake 更好,但这只是 Coffee Lake 默认禁用 LSD 的效果。 Skylake 在启用 LSD 的情况下进行了测试,之后英特尔由于安全问题通过微代码更新将其禁用。 Coffee Lake 在已知此问题后发布,因此开箱即用地禁用了 LSD。因此,对于此测试,Coffee Lake 使用 DSB(对于低于约 18 微指令的循环,仍然可以放入 DSB)或传统解码器(对于循环的其余部分),这会在小微指令数量下获得更好的结果LSD 施加开销的循环(有趣的是,对于较大的循环,LSD 和传统解码器恰好施加完全相同的开销,原因完全不同)。
最后,我们看一下 2 字节 NOP,它们的密度不足以阻止使用 DSB(因此这种情况更能反映典型代码)。
图 2.1:2 字节 nop 性能:
同样,结果与之前的图表相同。 Nehalem 仍然是每个周期 3 微指令的异常瓶颈。对于高达 60 左右 uops 的范围,除 Coffee Lake 之外的所有架构都在使用 LSD,我们看到 Sandy Bridge 和 Ivy Bridge 在这里的表现有点差,四舍五入到下一个周期,因此只能实现最大吞吐量 4 uops/cycle 如果循环中的微指令数是 4 的倍数。超过 32 微指令时,Haswell 和新的 uarchs 的 "unrolling" 特性没有任何效果,所以一切大致相同。
Sandy Bridge 实际上有几个微指令范围(例如,从 36 到 44 微指令),在这些范围内它的性能优于较新的架构。这似乎是因为并非所有环路都被 LSD 检测到,并且在这些范围内,环路由 DSB 提供服务。由于 DSB 通常更快,因此在这些情况下 Sandy Bridge 也更快。
英特尔的说法
您实际上可以在英特尔优化手册第 3.4.2.5 节中找到专门处理此主题的部分,正如 Andreas Abel 在评论中指出的那样。在那里,英特尔说:
The LSD holds micro-ops that construct small “infinite” loops. Micro-ops from the LSD are allocated in the out-of-order engine. The loop in the LSD ends with a taken branch to the beginning of the loop. The taken branch at the end of the loop is always the last micro-op allocated in the cycle. The instruction at the beginning of the loop is always allocated at the next cycle. If the code performance is bound by front end bandwidth, unused allocation slots result in a bubble in allocation, and can cause performance degrada- tion. Allocation bandwidth in Intel microarchitecture code name Sandy Bridge is four micro-ops per cycle. Performance is best, when the number of micro-ops in the LSD result in the least number of unused allo- cation slots. You can use loop unrolling to control the number of micro-ops that are in the LSD.
他们继续展示了一个示例,其中由于 LSD "rounding",将循环展开两倍对性能没有帮助,但展开三倍有效。该示例非常令人困惑,因为它实际上混合了两种效果,因为展开更多还减少了循环开销,因此减少了每次迭代的微指令数。一个更有趣的例子是,由于 LSD 舍入效应,展开循环 更少 次导致性能提高。
这部分似乎准确地描述了 Sandy Bridge 和 Ivy Bridge 中的行为。上面的结果表明这两种架构都按照描述的方式工作,并且分别为 4N+3、4N+2 或 4N+1 微指令的循环损失了 1、2 或 3 微指令执行槽。
Haswell 及更高版本的新性能尚未更新。如另一个答案中所述,性能比上述简单模型有所提高,并且行为更加复杂。
1 在 16 微秒处有一个奇怪的异常值,其中 Coffee Lake 的性能比所有其他架构都要差,甚至 Nehalem(回归大约 50%),但也许这个测量噪音?
TL;DR: 对于正好由 7 微指令组成的紧密循环,它会导致退休带宽利用率低下。考虑手动展开循环,因此循环将包含 12 uops
我最近面临着由 7 微指令组成的循环的退休带宽退化。在我自己做了一些研究之后,快速谷歌搜索将我带到了这个话题。这是我申请 Kaby Lake i7-8550U 的 2 美分 CPU:
正如@BeeOnRope 指出的那样,LSD 在 KbL i7-8550U
.
考虑以下 NASM 宏
;rdi = 1L << 31
%macro nops 1
align 32:
%%loop:
times %1 nop
dec rdi
ja %%loop
%endmacro
下面是 "average retirement rate" 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 uops/c。当 IDQ 溢出时,uops 缓存保持空闲状态,直到它可以再次传送 6 uops。
为了检查 uops 缓存如何保持空闲,让我们比较一下 idq.all_dsb_cycles_any_uops
和周期
传递给idq的微指令周期数等于7微指令循环的总周期数。相比之下,6 微指令循环的计数器明显不同。
要检查的关键计数器是 idq_uops_not_delivered.*
从 7 微指令的循环可以看出,重命名器采用 4|3 组,这导致退休带宽利用率低下。