为什么jnz需要2个周期才能在内循环中完成
Why jnz requires 2 cycles to complete in an inner loop
我在 IvyBridge 上。我发现 jnz
的性能行为在内循环和外循环中不一致。
下面的简单程序有一个固定大小为 16 的内循环:
global _start
_start:
mov rcx, 100000000
.loop_outer:
mov rax, 16
.loop_inner:
dec rax
jnz .loop_inner
dec rcx
jnz .loop_outer
xor edi, edi
mov eax, 60
syscall
perf
工具显示外循环运行 32c/iter。它表明 jnz
需要 2 个周期才能完成。
然后我在Agner的指令table中搜索,条件跳转有1-2"reciprocal throughput",注释"fast if no jump".
在这一点上,我开始相信上述行为在某种程度上是意料之中的。但是为什么jnz
在外层循环只需要1个循环就可以完成呢?
如果我完全删除 .loop_inner
部分,外循环运行 1c/iter。行为看起来不一致。
我在这里缺少什么?
编辑以获取更多信息:
上述程序的 perf
结果与命令:
perf stat -ecycles,branches,branch-misses,lsd.uops,uops_issued.any -r4 ./a.out
是:
3,215,921,579 cycles ( +- 0.11% ) (79.83%)
1,701,361,270 branches ( +- 0.02% ) (80.05%)
19,212 branch-misses # 0.00% of all branches ( +- 17.72% ) (80.09%)
31,052 lsd.uops ( +- 76.58% ) (80.09%)
1,803,009,428 uops_issued.any ( +- 0.08% ) (79.93%)
参考案例的perf
结果:
global _start
_start:
mov rcx, 100000000
.loop_outer:
mov rax, 16
dec rcx
jnz .loop_outer
xor edi, edi
mov eax, 60
syscall
是:
100,978,250 cycles ( +- 0.66% ) (75.75%)
100,606,742 branches ( +- 0.59% ) (75.74%)
1,825 branch-misses # 0.00% of all branches ( +- 13.15% ) (81.22%)
199,698,873 lsd.uops ( +- 0.07% ) (87.87%)
200,300,606 uops_issued.any ( +- 0.12% ) (79.42%)
所以原因很清楚:LSD 在嵌套情况下由于某种原因停止工作。减小内循环大小会稍微减轻速度,但不会完全减轻。
搜索 Intel "optimization manual",我发现如果循环包含 "more than eight taken branches",LSD 将不起作用。这在某种程度上解释了这种行为。
TL;DR: DSB似乎每隔一个周期只能传递一个内循环的跳转uop。 DSB-MITE 开关占执行时间的 9%。
简介 - 第 1 部分:了解 LSD 性能事件
我将首先讨论 LSD.UOPS
和 LSD.CYCLES_ACTIVE
性能事件发生的时间以及 LSD 在 IvB 和 SnB 微体系结构上的一些特性。一旦我们建立了这个基础,我们就可以回答这个问题。为此,我们可以使用专门设计的小段代码来准确确定这些事件何时发生。
根据文档:
LSD.UOPS
: Number of Uops delivered by the LSD.
LSD.CYCLES_ACTIVE
: Cycles Uops delivered by the LSD, but didn't come
from the decoder.
这些定义很有用,但正如您稍后将看到的那样,不够精确,无法回答您的问题。更好地了解这些事件很重要。此处提供的一些信息未被英特尔记录,这只是我对经验结果和我经历过的一些相关专利的最佳解释。虽然我无法找到描述 SnB 或更高版本微体系结构中 LSD 实现的具体专利。
以下每个基准测试都以包含基准测试名称的注释开头。除非另有说明,否则每次迭代都会对所有数字进行归一化处理。
; B1
----------------------------------------------------
mov rax, 100000000
.loop:
dec rax
jnz .loop
----------------------------------------------------
Metric | IvB | SnB
----------------------------------------------------
cycles | 0.90 | 1.00
LSD.UOPS | 0.99 | 1.99
LSD.CYCLES_ACTIVE | 0.49 | 0.99
CYCLE_ACTIVITY.CYCLES_NO_EXECUTE | 0.00 | 0.00
UOPS_ISSUED.STALL_CYCLES | 0.43 | 0.50
循环体中的两条指令都mac-fused到一个uop中。 IvB和SnB上只有一个执行端口可以执行跳转指令。因此,最大吞吐量应该是1c/iter。不过,出于某种原因,IvB 快了 10%。
根据,IvB 和 SnB 中的 LSD 不能跨循环体边界发出微指令,即使有可用的发布槽。由于循环包含一个 uop,我们预计 LSD 将在每个周期发出一个 uop,并且 LSD.CYCLES_ACTIVE
应该大约等于循环总数。
在 IvB 上,LSD.UOPS
符合预期。即LSD每周期会发出一个uop。请注意,由于循环数等于迭代次数,而迭代次数等于微指令数,我们可以等价地说 LSD 每次迭代发出一个微指令。本质上,大多数被执行的微指令都是从 LSD 发出的。但是,LSD.CYCLES_ACTIVE
大约是循环数的一半。这怎么可能?在这种情况下,从 LSD 发出的 uops 总数不应该只有一半吗?我认为这里发生的事情是循环基本上被展开两次并且每个周期发出两个微指令。尽管如此,每个周期只能执行一个 uop,但 RESOURCE_STALLS.RS
为零,表明 RS 永远不会变满。但是,RESOURCE_STALLS.ANY
大约是循环计数的一半。现在将所有这些放在一起,似乎 LSD 实际上每隔一个周期 发出 2 uops 并且每隔一个周期就会达到一些结构限制。 CYCLE_ACTIVITY.CYCLES_NO_EXECUTE
确认在任何给定周期,RS 中始终至少有一个读取 uop。以下实验将揭示展开的条件。
在 SnB 上,LSD.UOPS
显示从 LSD 发出的 uops 总数的两倍。另外 LSD.CYCLES_ACTIVE
表示 LSD 大部分时间都处于活动状态。 CYCLE_ACTIVITY.CYCLES_NO_EXECUTE
和 UOPS_ISSUED.STALL_CYCLES
与 IvB 相同。以下实验有助于理解正在发生的事情。看来实测的LSD.CYCLES_ACTIVE
等于真实的LSD.CYCLES_ACTIVE
+RESOURCE_STALLS.ANY
。因此,要得到真实的LSD.CYCLES_ACTIVE
,必须从测量的LSD.CYCLES_ACTIVE
中减去RESOURCE_STALLS.ANY
。这同样适用于 LSD.CYCLES_4_UOPS
。真正的LSD.UOPS
可以这样计算:
LSD.UOPS
实测 = LSD.UOPS
实际 + ((LSD.UOPS
实测/LSD.CYCLES_ACTIVE
实测)*RESOURCE_STALLS.ANY
)
因此,
LSD.UOPS
实际 = LSD.UOPS
实测 - ((LSD.UOPS
测量/LSD.CYCLES_ACTIVE
测量) * RESOURCE_STALLS.ANY
)
= LSD.UOPS
测量 * (1 - (RESOURCE_STALLS.ANY
/LSD.CYCLES_ACTIVE
测量))
对于我在 SnB 上 运行 的所有基准(包括此处未显示的基准),这些调整都是准确的。
请注意,SnB 上的 RESOURCE_STALLS.RS
和 RESOURCE_STALLS.ANY
与 IvB 相同。因此,就此特定基准而言,LSD 在 IvB 和 SnB 上的工作方式似乎相同,只是事件 LSD.UOPS
和 LSD.CYCLES_ACTIVE
的计算方式不同。
; B2
----------------------------------------------------
mov rax, 100000000
mov rbx, 0
.loop:
dec rbx
jz .loop
dec rax
jnz .loop
----------------------------------------------------
Metric | IvB | SnB
----------------------------------------------------
cycles | 1.98 | 2.00
LSD.UOPS | 1.92 | 3.99
LSD.CYCLES_ACTIVE | 0.94 | 1.99
CYCLE_ACTIVITY.CYCLES_NO_EXECUTE | 0.00 | 0.00
UOPS_ISSUED.STALL_CYCLES | 1.00 | 1.00
在B2中,每次迭代有2个微指令,都是跳转。第一个从未被采用,所以仍然只有一个循环。我们期望它在 2c/iter 时 运行,事实确实如此。 LSD.UOPS
表明大多数 uops 是从 LSD 发出的,但是 LSD.CYCLES_ACTIVE
表明 LSD 只有一半时间处于活动状态。这意味着循环没有展开。所以似乎展开只有在循环中只有一个uop时才会发生。
; B3
----------------------------------------------------
mov rax, 100000000
.loop:
dec rbx
dec rax
jnz .loop
----------------------------------------------------
Metric | IvB | SnB
----------------------------------------------------
cycles | 0.90 | 1.00
LSD.UOPS | 1.99 | 1.99
LSD.CYCLES_ACTIVE | 0.99 | 0.99
CYCLE_ACTIVITY.CYCLES_NO_EXECUTE | 0.00 | 0.00
UOPS_ISSUED.STALL_CYCLES | 0.00 | 0.00
这里也有2个微指令,但第一个是single-cycle ALU微指令,与跳转微指令无关。 B3帮助我们回答了以下两个问题:
- 如果跳转目标不是跳转uop,
LSD.UOPS
和LSD.CYCLES_ACTIVE
在SnB上还会算两次吗?
- 如果循环包含 2 个微指令,其中只有一个是跳转,LSD 会展开循环吗?
B3 显示两个问题的答案都是 "No."
UOPS_ISSUED.STALL_CYCLES
表示LSD在一个周期内发出两次跳转uop,只会拖延一个周期。这在 B3 中从来没有发生过,所以没有摊位。
; B4
----------------------------------------------------
mov rax, 100000000
.loop:
add rbx, qword [buf]
dec rax
jnz .loop
----------------------------------------------------
Metric | IvB | SnB
----------------------------------------------------
cycles | 0.90 | 1.00
LSD.UOPS | 1.99 | 2.00
LSD.CYCLES_ACTIVE | 0.99 | 1.00
CYCLE_ACTIVITY.CYCLES_NO_EXECUTE | 0.00 | 0.00
UOPS_ISSUED.STALL_CYCLES | 0.00 | 0.00
B4 有一个额外的扭曲;它在融合域中包含 2 微指令,但在融合域中包含 3 微指令,因为 load-ALU 指令在 RS 中未融合。在以前的基准测试中,没有 micro-fused uops,只有 macro-fused uops。这里的目标是查看 LSD 如何处理 micro-fused 微指令。
LSD.UOPS
表明 load-ALU 指令的两个 uop 已经消耗了一个 issue slot(融合跳转 uop 只消耗了一个 slot)。此外,由于 LSD.CYCLES_ACTIVE
等于 cycles
,因此未发生展开。循环吞吐量符合预期。
; B5
----------------------------------------------------
mov rax, 100000000
.loop:
jmp .next
.next:
dec rax
jnz .loop
----------------------------------------------------
Metric | IvB | SnB
----------------------------------------------------
cycles | 2.00 | 2.00
LSD.UOPS | 1.91 | 3.99
LSD.CYCLES_ACTIVE | 0.96 | 1.99
CYCLE_ACTIVITY.CYCLES_NO_EXECUTE | 0.00 | 0.00
UOPS_ISSUED.STALL_CYCLES | 1.00 | 1.00
B5 是我们需要的最后一个基准。它与 B2 的相似之处在于它包含两个分支微指令。但是,B5 中的一个跳跃动作是向前无条件跳跃。结果与 B2 相同,表明跳转 uop 是否有条件无关紧要。如果第一个跳转 uop 是有条件的而第二个不是,这也是这种情况。
简介 - 第 2 部分:LSD 中的分支预测
LSD是在uop队列(IDQ)中实现的机制,可以提高性能并降低功耗(从而减少热量排放)。 它可以提高性能,因为前端中存在的一些限制可能会在 uop 队列中放宽。特别是,在 SnB 和 IvB 上,MITE 和 DSB 路径都有最大吞吐量4uops/c,但按字节计算,分别是16B/c和32B/c。 uop队列带宽也是4uops/c,但对字节数没有限制。只要 LSD 从 uop 队列、前端(即获取和解码单元)甚至 unneeded logic downstream from the IDQ can be powered down. Prior to Nehalem, the LSD was implemented in the IQ unit. Starting with Haswell, the LSD supports loops that contain uops from the MSROM 发出 uops。 Skylake 处理器中的 LSD 被禁用,因为它显然有问题。
循环通常包含至少一个条件分支。 LSD 实质上监视向后条件分支并尝试确定构成循环的微指令序列。如果 LSD 花费太多时间来检测环路,则性能可能会降低并且可能会浪费功率。另一方面,如果 LSD 过早地锁定一个循环并尝试重放它,则循环的条件跳转实际上可能会失败。这只能在执行条件跳转后才能检测到,这意味着后面的 uops 可能已经发出并被调度执行。所有这些 uops 都需要被刷新,并且前端需要被激活以从正确的路径获取 uops。因此,如果使用 LSD 带来的性能提升不超过因可能错误预测最后一次执行退出循环的条件分支而导致的性能下降,则可能会出现显着的性能损失。
我们已经知道,当迭代总数不超过某个小数时,SnB 及更高版本的分支预测单元 (BPU) 可以正确预测循环的条件分支何时失败,之后 BPU 假设循环将永远迭代。如果 LSD 使用 BPU 的复杂功能来预测锁定循环何时终止,它应该能够正确预测相同的情况。 LSD 也有可能使用自己的分支预测器,这可能要简单得多。让我们一探究竟。
mov rcx, 100000000/(IC+3)
.loop_outer:
mov rax, IC
mov rbx, 1
.loop_inner:
dec rax
jnz .loop_inner
dec rcx
jnz .loop_outer
令OC
和IC
分别表示外迭代次数和内迭代次数。这些相关如下:
OC
= 100000000/(IC
+3) 其中 IC
> 0
对于任何给定的 IC
,退役的微指令总数是相同的。此外,融合域中的微指令数等于未融合域中的微指令数。这很好,因为它确实简化了分析,并允许我们在 IC
.
的不同值之间进行公平的性能比较
与题中的代码相比,多了一条指令,mov rbx, 1
,所以外层循环的总微指令数恰好是4微指令。这使我们能够在 LSD.CYCLES_ACTIVE
和 BR_MISP_RETIRED.CONDITIONAL
之外使用 LSD.CYCLES_4_UOPS
性能事件。请注意,由于只有一个分支执行端口,因此每个外循环迭代至少需要 2 个周期(或根据 Agner 的 table,1-2 个周期)。另见:.
总的跳转次数为:
OC
+ IC
*OC
= 100M/(IC
+3) + IC
*100M/(IC
+ 3)
= 100M(IC
+1)/(IC
+3)
假设t 最大跳转 uop 吞吐量为每个周期 1 个,最佳执行时间为 100M(IC
+1)/(IC
+3) 个周期。在 IvB 上,如果我们想要严格的话,我们可以使用 0.9/c 的最大跳跃 uop 吞吐量。将其除以内部迭代次数会很有用:
OPT
= (100M(IC
+1)/(IC
+3)) / (100MIC
/(IC
+3 )) =
100M(IC
+1) * (IC
+3) / (IC
+3) * 100MIC
=
(IC
+1)/IC
= 1 + 1/IC
因此,1 < OPT
<= 1.5 for IC
> 1. 设计 LSD 的人可以使用它来比较 LSD 的不同设计。我们很快也会用到它。换句话说,当总循环次数除以总跳跃次数为 1(或 IvB 为 0.9)时,性能最佳。
假设两个跳跃的预测是独立的并且假设jnz .loop_outer
很容易预测table,性能取决于jnz .loop_inner
的预测。如果预测错误将控制更改为锁定循环外的 uop,LSD 将终止循环并尝试检测另一个循环。 LSD 可以表示为具有三个状态的状态机。在一种状态下,LSD 正在寻找一种循环行为。在第二种状态下,LSD 正在学习循环的边界和迭代次数。在第三种状态下,LSD 正在重放循环。当循环存在时,状态从第三变到第一。
正如我们从之前的一组实验中了解到的,当有 backend-related 问题停顿时,SnB 上会有额外的 LSD 事件。因此,需要相应地理解这些数字。注意 IC
=1 的情况在上一节中没有测试过。将在这里讨论。还记得,在 IvB 和 SnB 上,内部循环可能会展开。外循环永远不会展开,因为它包含多个微指令。顺便说一下,LSD.CYCLES_4_UOPS
按预期工作(抱歉,没有惊喜)。
下图显示了原始结果。我只在 IvB 和 SnB 上分别显示了 IC
=13 和 IC
=9 的结果。我将在下一节讨论更大的值会发生什么。请注意,当分母为零时,无法计算该值,因此不会绘制它。
LSD.UOPS/100M
是LSD发出的微指令数占微指令总数的比例。 LSD.UOPS/OC
是每次外部迭代从 LSD 发出的平均微指令数。 LSD.UOPS/(OC*IC)
是每次内部迭代从 LSD 发出的平均微指令数。 BR_MISP_RETIRED.CONDITIONAL/OC
是每次外部迭代被错误预测的退役条件分支的平均数量,对于所有 IC
.
,在 IvB 和 SnB 上显然为零
对于 IvB 上的 IC
=1,所有 uops 都是从 LSD 发出的。始终不采用内部条件分支。第二张图中显示的 LSD.CYCLES_4_UOPS/LSD.CYCLES_ACTIVE
指标显示,在 LSD 处于活动状态的所有周期中,LSD 每个周期发出 4 微指令。我们从前面的实验中了解到,当LSD在同一个周期发出2次跳转指令时,由于结构上的限制,它无法在下一个周期发出跳转指令,因此会停顿。 LSD.CYCLES_ACTIVE/cycles
表明 LSD(几乎)每隔一个周期就会停止。我们预计执行一次外部迭代大约需要 2 个周期,但 cycles
显示大约需要 1.8 个周期。这大概和我们之前看到的IvB上0.9jump uop throughput有关
SnB 上 IC
=1 的情况类似,除了两点。首先,一个外部循环实际上需要 2 个周期,而不是 1.8。其次,所有三个 LSD 事件计数都是预期的两倍。它们可以按照上一节中的讨论进行调整。
当 IC
>1 时,分支预测特别有趣。我们来详细分析一下 IC
=2 的情况。 LSD.CYCLES_ACTIVE
和 LSD.CYCLES_4_UOPS
表明,在大约 32% 的所有周期中,LSD 处于活动状态,而在这些周期的 50% 中,LSD 每个周期发出 4 微指令。因此,要么存在错误预测,要么 LSD 在循环检测状态或学习状态中花费了大量时间。尽管如此,cycles
/(OC
*IC
) 大约是 1.6,或者换句话说,cycles
/jumps
是 1.07,接近最优表现。很难弄清楚哪些微指令是从 LSD 以 4 为一组发出的,哪些微指令是从 LSD 以小于 4 的组发出的。事实上,我们不知道在存在 LSD 错误预测的情况下如何计算 LSD 事件。潜在的展开增加了另一个层次的复杂性。 LSD 事件计数可以被认为是 LSD 发出的有用微指令和 LSD 发出有用微指令的周期的上限。
随着 IC
的增加,LSD.CYCLES_ACTIVE
和 LSD.CYCLES_4_UOPS
都会下降,性能会缓慢但持续地下降(请记住 cycles
/(OC
*IC
) 应该与 OPT
) 进行比较。就好像最后一个内循环迭代被错误预测了ed,但它的错误预测惩罚随着 IC
的增加而增加。请注意,BPU 始终正确预测内循环迭代次数。
答案
我将讨论任何 IC
会发生什么,为什么性能会随着 IC
变大,以及性能的上限和下限是什么。本节将使用以下代码:
mov rcx, 100000000/(IC+2)
.loop_outer:
mov rax, IC
.loop_inner:
dec rax
jnz .loop_inner
dec rcx
jnz .loop_outer
这与问题中的代码基本相同。唯一的区别是调整外部迭代次数以保持相同的动态微指令数。请注意,LSD.CYCLES_4_UOPS
在这种情况下是无用的,因为 LSD 在任何周期中都不会发出 4 微指令。以下所有数字仅适用于 IvB。不过不用担心,文中会提到 SnB 有何不同。
当IC
=1时,cycles
/jumps为0.7(SnB为1.0),甚至低于0.9。我不知道这个吞吐量是如何实现的。性能随 IC
的较大值而降低,这与 LSD 活动周期的减少相关。当 IC
=13-27(SnB 上为 9-27)时,LSD 发出零微指令。我认为在这个范围内,LSD 认为由于错误预测最后一次内部迭代而导致的性能影响大于某个阈值,它决定永远不锁定循环并记住它的决定。当 IC
<13 时,LSD 似乎具有侵略性并且可能认为循环更具有预测性 table。对于 IC
>27,LSD 活动周期计数缓慢增长,这与性能的逐渐提高相关。尽管图中未显示,但随着 IC
的增长远远超过 64,大部分 uops 将来自 LSD,并且 cycles
/jumps 稳定在 0.9。
IC
=13-27 范围内的结果特别有用。问题停顿周期大约是总周期数的一半,也等于调度停顿周期。正是由于这个原因,内部循环以 2.0c/iter 执行;因为内循环的跳转指令每隔一个周期就是 issued/dispatched。当 LSD 未激活时,微指令可以来自 DSB、MITE 或 MSROM。我们的循环不需要微码辅助,因此 DSB、MITE 或两者中可能存在限制。我们可以进一步调查以确定使用前端性能事件的限制在哪里。我已经这样做了,结果显示大约 80-90% 的微指令来自 DSB。 DSB 本身有很多局限性,循环似乎正在影响其中一个。似乎 DSB 需要 2 个周期来传递以自身为目标的跳转指令。此外,对于整个 IC
范围,由于 MITE-DSB 切换引起的停顿占所有周期的 9%。同样,这些转换的原因是 DSB 本身的限制。请注意,高达 20% 的数据来自 MITE 路径。假设 uops 不超过 MITE 路径的 16B/c 带宽,我认为如果 DSB 不存在,循环将以 1c/iter 执行。
上图还显示了 BPU 错误预测率(每次外循环迭代)。在IvB上,IC
=1-33时为零,IC
=21时除外,IC
=34-45时为0-1,IC
时为1 >46。在 SnB 上,IC
=1-33 为零,否则为 1。
(部分回答/猜测我在哈迪发布详细分析之前没有写完;其中一些是从评论中继续的)
Agner's statement "the loop buffer has no measurable effect in the cases where the uop cache is not a bottleneck..." is wrong? Because this is certainly a measurable effect and the uop cache isn't bottleneck because the cache has ~1.5K capacity.
是的,Agner 称它为环回缓冲区。 他的说法是将 LSD 添加到设计中不会加速任何代码。但是,是的,对于非常紧密的循环来说似乎是错误的,至少对于嵌套循环来说是这样。显然 SnB/IvB 确实需要循环缓冲区来发出或执行 1c/iter 循环。除非微架构瓶颈是在分支后从 uop 缓存中获取 uops,在这种情况下,他的警告涵盖了这一点。
除了 uop 缓存未命中之外,在其他情况下读取 uop 缓存可能成为瓶颈。例如如果 uops 由于对齐效果而不能很好地打包,或者如果它们使用大立即数 and/or 位移需要额外的周期才能从 uop 缓存中读取。有关这些效果的更多详细信息,请参阅 Agner Fog's uarch guide 的 Sandybridge 部分。您假设容量(如果它们完美包装则高达 1.5k uops)是它可能变慢的唯一原因是非常错误的。
顺便说一句,Skylake 的微代码更新完全禁用了 LSD 以修复部分寄存器合并错误,错误 SKL1501,以及除非一个小循环跨越 32B 边界并需要 2 个缓存行,否则实际上效果很小。
但 Agner 列出 JMP rel8/32
并在 HSW/SKL 上将 JCC 吞吐量视为 1-2 个周期,而在 IvB 上仅为 2 个周期。因此,除了 LSD 本身之外,自 IvB 以来,关于采取的分支的一些事情可能已经加速。
可能 CPU 的某些部分不是 LSD,它也有一个特殊情况,用于长 运行 宁小循环,让他们 运行 1在 Haswell 及更高版本上按时钟跳转。我还没有测试什么条件导致 1 vs. 2 循环在 HSW/SKL 上采用分支吞吐量。另请注意,Agner 在错误 SKL150 的微代码更新之前进行了测量。
脚注 1:请参阅 ,并注意 SKX 和 Kaby Lake 附带的微代码已经包含此内容。它最终在 CPU 中重新启用,例如 CannonLake / Ice Lake,它修复了有问题的硬连线逻辑,因此 LSD 可以安全地再次启用。
(我之前认为 Coffee Lake 已经重新启用了 LSD,但它似乎没有 - wikichip 明确表示它仍然被禁用,所以我认为这是在纠正一些早期的报告,即它已重新启用。CFL不过,确实修复了 L1TF 和 Meltdown 漏洞,使得针对这些漏洞的软件缓解变得不必要。)
我在 IvyBridge 上。我发现 jnz
的性能行为在内循环和外循环中不一致。
下面的简单程序有一个固定大小为 16 的内循环:
global _start
_start:
mov rcx, 100000000
.loop_outer:
mov rax, 16
.loop_inner:
dec rax
jnz .loop_inner
dec rcx
jnz .loop_outer
xor edi, edi
mov eax, 60
syscall
perf
工具显示外循环运行 32c/iter。它表明 jnz
需要 2 个周期才能完成。
然后我在Agner的指令table中搜索,条件跳转有1-2"reciprocal throughput",注释"fast if no jump".
在这一点上,我开始相信上述行为在某种程度上是意料之中的。但是为什么jnz
在外层循环只需要1个循环就可以完成呢?
如果我完全删除 .loop_inner
部分,外循环运行 1c/iter。行为看起来不一致。
我在这里缺少什么?
编辑以获取更多信息:
上述程序的 perf
结果与命令:
perf stat -ecycles,branches,branch-misses,lsd.uops,uops_issued.any -r4 ./a.out
是:
3,215,921,579 cycles ( +- 0.11% ) (79.83%)
1,701,361,270 branches ( +- 0.02% ) (80.05%)
19,212 branch-misses # 0.00% of all branches ( +- 17.72% ) (80.09%)
31,052 lsd.uops ( +- 76.58% ) (80.09%)
1,803,009,428 uops_issued.any ( +- 0.08% ) (79.93%)
参考案例的perf
结果:
global _start
_start:
mov rcx, 100000000
.loop_outer:
mov rax, 16
dec rcx
jnz .loop_outer
xor edi, edi
mov eax, 60
syscall
是:
100,978,250 cycles ( +- 0.66% ) (75.75%)
100,606,742 branches ( +- 0.59% ) (75.74%)
1,825 branch-misses # 0.00% of all branches ( +- 13.15% ) (81.22%)
199,698,873 lsd.uops ( +- 0.07% ) (87.87%)
200,300,606 uops_issued.any ( +- 0.12% ) (79.42%)
所以原因很清楚:LSD 在嵌套情况下由于某种原因停止工作。减小内循环大小会稍微减轻速度,但不会完全减轻。
搜索 Intel "optimization manual",我发现如果循环包含 "more than eight taken branches",LSD 将不起作用。这在某种程度上解释了这种行为。
TL;DR: DSB似乎每隔一个周期只能传递一个内循环的跳转uop。 DSB-MITE 开关占执行时间的 9%。
简介 - 第 1 部分:了解 LSD 性能事件
我将首先讨论 LSD.UOPS
和 LSD.CYCLES_ACTIVE
性能事件发生的时间以及 LSD 在 IvB 和 SnB 微体系结构上的一些特性。一旦我们建立了这个基础,我们就可以回答这个问题。为此,我们可以使用专门设计的小段代码来准确确定这些事件何时发生。
根据文档:
LSD.UOPS
: Number of Uops delivered by the LSD.
LSD.CYCLES_ACTIVE
: Cycles Uops delivered by the LSD, but didn't come from the decoder.
这些定义很有用,但正如您稍后将看到的那样,不够精确,无法回答您的问题。更好地了解这些事件很重要。此处提供的一些信息未被英特尔记录,这只是我对经验结果和我经历过的一些相关专利的最佳解释。虽然我无法找到描述 SnB 或更高版本微体系结构中 LSD 实现的具体专利。
以下每个基准测试都以包含基准测试名称的注释开头。除非另有说明,否则每次迭代都会对所有数字进行归一化处理。
; B1
----------------------------------------------------
mov rax, 100000000
.loop:
dec rax
jnz .loop
----------------------------------------------------
Metric | IvB | SnB
----------------------------------------------------
cycles | 0.90 | 1.00
LSD.UOPS | 0.99 | 1.99
LSD.CYCLES_ACTIVE | 0.49 | 0.99
CYCLE_ACTIVITY.CYCLES_NO_EXECUTE | 0.00 | 0.00
UOPS_ISSUED.STALL_CYCLES | 0.43 | 0.50
循环体中的两条指令都mac-fused到一个uop中。 IvB和SnB上只有一个执行端口可以执行跳转指令。因此,最大吞吐量应该是1c/iter。不过,出于某种原因,IvB 快了 10%。
根据LSD.CYCLES_ACTIVE
应该大约等于循环总数。
在 IvB 上,LSD.UOPS
符合预期。即LSD每周期会发出一个uop。请注意,由于循环数等于迭代次数,而迭代次数等于微指令数,我们可以等价地说 LSD 每次迭代发出一个微指令。本质上,大多数被执行的微指令都是从 LSD 发出的。但是,LSD.CYCLES_ACTIVE
大约是循环数的一半。这怎么可能?在这种情况下,从 LSD 发出的 uops 总数不应该只有一半吗?我认为这里发生的事情是循环基本上被展开两次并且每个周期发出两个微指令。尽管如此,每个周期只能执行一个 uop,但 RESOURCE_STALLS.RS
为零,表明 RS 永远不会变满。但是,RESOURCE_STALLS.ANY
大约是循环计数的一半。现在将所有这些放在一起,似乎 LSD 实际上每隔一个周期 发出 2 uops 并且每隔一个周期就会达到一些结构限制。 CYCLE_ACTIVITY.CYCLES_NO_EXECUTE
确认在任何给定周期,RS 中始终至少有一个读取 uop。以下实验将揭示展开的条件。
在 SnB 上,LSD.UOPS
显示从 LSD 发出的 uops 总数的两倍。另外 LSD.CYCLES_ACTIVE
表示 LSD 大部分时间都处于活动状态。 CYCLE_ACTIVITY.CYCLES_NO_EXECUTE
和 UOPS_ISSUED.STALL_CYCLES
与 IvB 相同。以下实验有助于理解正在发生的事情。看来实测的LSD.CYCLES_ACTIVE
等于真实的LSD.CYCLES_ACTIVE
+RESOURCE_STALLS.ANY
。因此,要得到真实的LSD.CYCLES_ACTIVE
,必须从测量的LSD.CYCLES_ACTIVE
中减去RESOURCE_STALLS.ANY
。这同样适用于 LSD.CYCLES_4_UOPS
。真正的LSD.UOPS
可以这样计算:
LSD.UOPS
实测 = LSD.UOPS
实际 + ((LSD.UOPS
实测/LSD.CYCLES_ACTIVE
实测)*RESOURCE_STALLS.ANY
)
因此,
LSD.UOPS
实际 = LSD.UOPS
实测 - ((LSD.UOPS
测量/LSD.CYCLES_ACTIVE
测量) * RESOURCE_STALLS.ANY
)
= LSD.UOPS
测量 * (1 - (RESOURCE_STALLS.ANY
/LSD.CYCLES_ACTIVE
测量))
对于我在 SnB 上 运行 的所有基准(包括此处未显示的基准),这些调整都是准确的。
请注意,SnB 上的 RESOURCE_STALLS.RS
和 RESOURCE_STALLS.ANY
与 IvB 相同。因此,就此特定基准而言,LSD 在 IvB 和 SnB 上的工作方式似乎相同,只是事件 LSD.UOPS
和 LSD.CYCLES_ACTIVE
的计算方式不同。
; B2
----------------------------------------------------
mov rax, 100000000
mov rbx, 0
.loop:
dec rbx
jz .loop
dec rax
jnz .loop
----------------------------------------------------
Metric | IvB | SnB
----------------------------------------------------
cycles | 1.98 | 2.00
LSD.UOPS | 1.92 | 3.99
LSD.CYCLES_ACTIVE | 0.94 | 1.99
CYCLE_ACTIVITY.CYCLES_NO_EXECUTE | 0.00 | 0.00
UOPS_ISSUED.STALL_CYCLES | 1.00 | 1.00
在B2中,每次迭代有2个微指令,都是跳转。第一个从未被采用,所以仍然只有一个循环。我们期望它在 2c/iter 时 运行,事实确实如此。 LSD.UOPS
表明大多数 uops 是从 LSD 发出的,但是 LSD.CYCLES_ACTIVE
表明 LSD 只有一半时间处于活动状态。这意味着循环没有展开。所以似乎展开只有在循环中只有一个uop时才会发生。
; B3
----------------------------------------------------
mov rax, 100000000
.loop:
dec rbx
dec rax
jnz .loop
----------------------------------------------------
Metric | IvB | SnB
----------------------------------------------------
cycles | 0.90 | 1.00
LSD.UOPS | 1.99 | 1.99
LSD.CYCLES_ACTIVE | 0.99 | 0.99
CYCLE_ACTIVITY.CYCLES_NO_EXECUTE | 0.00 | 0.00
UOPS_ISSUED.STALL_CYCLES | 0.00 | 0.00
这里也有2个微指令,但第一个是single-cycle ALU微指令,与跳转微指令无关。 B3帮助我们回答了以下两个问题:
- 如果跳转目标不是跳转uop,
LSD.UOPS
和LSD.CYCLES_ACTIVE
在SnB上还会算两次吗? - 如果循环包含 2 个微指令,其中只有一个是跳转,LSD 会展开循环吗?
B3 显示两个问题的答案都是 "No."
UOPS_ISSUED.STALL_CYCLES
表示LSD在一个周期内发出两次跳转uop,只会拖延一个周期。这在 B3 中从来没有发生过,所以没有摊位。
; B4
----------------------------------------------------
mov rax, 100000000
.loop:
add rbx, qword [buf]
dec rax
jnz .loop
----------------------------------------------------
Metric | IvB | SnB
----------------------------------------------------
cycles | 0.90 | 1.00
LSD.UOPS | 1.99 | 2.00
LSD.CYCLES_ACTIVE | 0.99 | 1.00
CYCLE_ACTIVITY.CYCLES_NO_EXECUTE | 0.00 | 0.00
UOPS_ISSUED.STALL_CYCLES | 0.00 | 0.00
B4 有一个额外的扭曲;它在融合域中包含 2 微指令,但在融合域中包含 3 微指令,因为 load-ALU 指令在 RS 中未融合。在以前的基准测试中,没有 micro-fused uops,只有 macro-fused uops。这里的目标是查看 LSD 如何处理 micro-fused 微指令。
LSD.UOPS
表明 load-ALU 指令的两个 uop 已经消耗了一个 issue slot(融合跳转 uop 只消耗了一个 slot)。此外,由于 LSD.CYCLES_ACTIVE
等于 cycles
,因此未发生展开。循环吞吐量符合预期。
; B5
----------------------------------------------------
mov rax, 100000000
.loop:
jmp .next
.next:
dec rax
jnz .loop
----------------------------------------------------
Metric | IvB | SnB
----------------------------------------------------
cycles | 2.00 | 2.00
LSD.UOPS | 1.91 | 3.99
LSD.CYCLES_ACTIVE | 0.96 | 1.99
CYCLE_ACTIVITY.CYCLES_NO_EXECUTE | 0.00 | 0.00
UOPS_ISSUED.STALL_CYCLES | 1.00 | 1.00
B5 是我们需要的最后一个基准。它与 B2 的相似之处在于它包含两个分支微指令。但是,B5 中的一个跳跃动作是向前无条件跳跃。结果与 B2 相同,表明跳转 uop 是否有条件无关紧要。如果第一个跳转 uop 是有条件的而第二个不是,这也是这种情况。
简介 - 第 2 部分:LSD 中的分支预测
LSD是在uop队列(IDQ)中实现的机制,可以提高性能并降低功耗(从而减少热量排放)。 它可以提高性能,因为前端中存在的一些限制可能会在 uop 队列中放宽。特别是,在 SnB 和 IvB 上,MITE 和 DSB 路径都有最大吞吐量4uops/c,但按字节计算,分别是16B/c和32B/c。 uop队列带宽也是4uops/c,但对字节数没有限制。只要 LSD 从 uop 队列、前端(即获取和解码单元)甚至 unneeded logic downstream from the IDQ can be powered down. Prior to Nehalem, the LSD was implemented in the IQ unit. Starting with Haswell, the LSD supports loops that contain uops from the MSROM 发出 uops。 Skylake 处理器中的 LSD 被禁用,因为它显然有问题。
循环通常包含至少一个条件分支。 LSD 实质上监视向后条件分支并尝试确定构成循环的微指令序列。如果 LSD 花费太多时间来检测环路,则性能可能会降低并且可能会浪费功率。另一方面,如果 LSD 过早地锁定一个循环并尝试重放它,则循环的条件跳转实际上可能会失败。这只能在执行条件跳转后才能检测到,这意味着后面的 uops 可能已经发出并被调度执行。所有这些 uops 都需要被刷新,并且前端需要被激活以从正确的路径获取 uops。因此,如果使用 LSD 带来的性能提升不超过因可能错误预测最后一次执行退出循环的条件分支而导致的性能下降,则可能会出现显着的性能损失。
我们已经知道,当迭代总数不超过某个小数时,SnB 及更高版本的分支预测单元 (BPU) 可以正确预测循环的条件分支何时失败,之后 BPU 假设循环将永远迭代。如果 LSD 使用 BPU 的复杂功能来预测锁定循环何时终止,它应该能够正确预测相同的情况。 LSD 也有可能使用自己的分支预测器,这可能要简单得多。让我们一探究竟。
mov rcx, 100000000/(IC+3)
.loop_outer:
mov rax, IC
mov rbx, 1
.loop_inner:
dec rax
jnz .loop_inner
dec rcx
jnz .loop_outer
令OC
和IC
分别表示外迭代次数和内迭代次数。这些相关如下:
OC
= 100000000/(IC
+3) 其中 IC
> 0
对于任何给定的 IC
,退役的微指令总数是相同的。此外,融合域中的微指令数等于未融合域中的微指令数。这很好,因为它确实简化了分析,并允许我们在 IC
.
与题中的代码相比,多了一条指令,mov rbx, 1
,所以外层循环的总微指令数恰好是4微指令。这使我们能够在 LSD.CYCLES_ACTIVE
和 BR_MISP_RETIRED.CONDITIONAL
之外使用 LSD.CYCLES_4_UOPS
性能事件。请注意,由于只有一个分支执行端口,因此每个外循环迭代至少需要 2 个周期(或根据 Agner 的 table,1-2 个周期)。另见:
总的跳转次数为:
OC
+ IC
*OC
= 100M/(IC
+3) + IC
*100M/(IC
+ 3)
= 100M(IC
+1)/(IC
+3)
假设t 最大跳转 uop 吞吐量为每个周期 1 个,最佳执行时间为 100M(IC
+1)/(IC
+3) 个周期。在 IvB 上,如果我们想要严格的话,我们可以使用 0.9/c 的最大跳跃 uop 吞吐量。将其除以内部迭代次数会很有用:
OPT
= (100M(IC
+1)/(IC
+3)) / (100MIC
/(IC
+3 )) =
100M(IC
+1) * (IC
+3) / (IC
+3) * 100MIC
=
(IC
+1)/IC
= 1 + 1/IC
因此,1 < OPT
<= 1.5 for IC
> 1. 设计 LSD 的人可以使用它来比较 LSD 的不同设计。我们很快也会用到它。换句话说,当总循环次数除以总跳跃次数为 1(或 IvB 为 0.9)时,性能最佳。
假设两个跳跃的预测是独立的并且假设jnz .loop_outer
很容易预测table,性能取决于jnz .loop_inner
的预测。如果预测错误将控制更改为锁定循环外的 uop,LSD 将终止循环并尝试检测另一个循环。 LSD 可以表示为具有三个状态的状态机。在一种状态下,LSD 正在寻找一种循环行为。在第二种状态下,LSD 正在学习循环的边界和迭代次数。在第三种状态下,LSD 正在重放循环。当循环存在时,状态从第三变到第一。
正如我们从之前的一组实验中了解到的,当有 backend-related 问题停顿时,SnB 上会有额外的 LSD 事件。因此,需要相应地理解这些数字。注意 IC
=1 的情况在上一节中没有测试过。将在这里讨论。还记得,在 IvB 和 SnB 上,内部循环可能会展开。外循环永远不会展开,因为它包含多个微指令。顺便说一下,LSD.CYCLES_4_UOPS
按预期工作(抱歉,没有惊喜)。
下图显示了原始结果。我只在 IvB 和 SnB 上分别显示了 IC
=13 和 IC
=9 的结果。我将在下一节讨论更大的值会发生什么。请注意,当分母为零时,无法计算该值,因此不会绘制它。
LSD.UOPS/100M
是LSD发出的微指令数占微指令总数的比例。 LSD.UOPS/OC
是每次外部迭代从 LSD 发出的平均微指令数。 LSD.UOPS/(OC*IC)
是每次内部迭代从 LSD 发出的平均微指令数。 BR_MISP_RETIRED.CONDITIONAL/OC
是每次外部迭代被错误预测的退役条件分支的平均数量,对于所有 IC
.
对于 IvB 上的 IC
=1,所有 uops 都是从 LSD 发出的。始终不采用内部条件分支。第二张图中显示的 LSD.CYCLES_4_UOPS/LSD.CYCLES_ACTIVE
指标显示,在 LSD 处于活动状态的所有周期中,LSD 每个周期发出 4 微指令。我们从前面的实验中了解到,当LSD在同一个周期发出2次跳转指令时,由于结构上的限制,它无法在下一个周期发出跳转指令,因此会停顿。 LSD.CYCLES_ACTIVE/cycles
表明 LSD(几乎)每隔一个周期就会停止。我们预计执行一次外部迭代大约需要 2 个周期,但 cycles
显示大约需要 1.8 个周期。这大概和我们之前看到的IvB上0.9jump uop throughput有关
SnB 上 IC
=1 的情况类似,除了两点。首先,一个外部循环实际上需要 2 个周期,而不是 1.8。其次,所有三个 LSD 事件计数都是预期的两倍。它们可以按照上一节中的讨论进行调整。
当 IC
>1 时,分支预测特别有趣。我们来详细分析一下 IC
=2 的情况。 LSD.CYCLES_ACTIVE
和 LSD.CYCLES_4_UOPS
表明,在大约 32% 的所有周期中,LSD 处于活动状态,而在这些周期的 50% 中,LSD 每个周期发出 4 微指令。因此,要么存在错误预测,要么 LSD 在循环检测状态或学习状态中花费了大量时间。尽管如此,cycles
/(OC
*IC
) 大约是 1.6,或者换句话说,cycles
/jumps
是 1.07,接近最优表现。很难弄清楚哪些微指令是从 LSD 以 4 为一组发出的,哪些微指令是从 LSD 以小于 4 的组发出的。事实上,我们不知道在存在 LSD 错误预测的情况下如何计算 LSD 事件。潜在的展开增加了另一个层次的复杂性。 LSD 事件计数可以被认为是 LSD 发出的有用微指令和 LSD 发出有用微指令的周期的上限。
随着 IC
的增加,LSD.CYCLES_ACTIVE
和 LSD.CYCLES_4_UOPS
都会下降,性能会缓慢但持续地下降(请记住 cycles
/(OC
*IC
) 应该与 OPT
) 进行比较。就好像最后一个内循环迭代被错误预测了ed,但它的错误预测惩罚随着 IC
的增加而增加。请注意,BPU 始终正确预测内循环迭代次数。
答案
我将讨论任何 IC
会发生什么,为什么性能会随着 IC
变大,以及性能的上限和下限是什么。本节将使用以下代码:
mov rcx, 100000000/(IC+2)
.loop_outer:
mov rax, IC
.loop_inner:
dec rax
jnz .loop_inner
dec rcx
jnz .loop_outer
这与问题中的代码基本相同。唯一的区别是调整外部迭代次数以保持相同的动态微指令数。请注意,LSD.CYCLES_4_UOPS
在这种情况下是无用的,因为 LSD 在任何周期中都不会发出 4 微指令。以下所有数字仅适用于 IvB。不过不用担心,文中会提到 SnB 有何不同。
当IC
=1时,cycles
/jumps为0.7(SnB为1.0),甚至低于0.9。我不知道这个吞吐量是如何实现的。性能随 IC
的较大值而降低,这与 LSD 活动周期的减少相关。当 IC
=13-27(SnB 上为 9-27)时,LSD 发出零微指令。我认为在这个范围内,LSD 认为由于错误预测最后一次内部迭代而导致的性能影响大于某个阈值,它决定永远不锁定循环并记住它的决定。当 IC
<13 时,LSD 似乎具有侵略性并且可能认为循环更具有预测性 table。对于 IC
>27,LSD 活动周期计数缓慢增长,这与性能的逐渐提高相关。尽管图中未显示,但随着 IC
的增长远远超过 64,大部分 uops 将来自 LSD,并且 cycles
/jumps 稳定在 0.9。
IC
=13-27 范围内的结果特别有用。问题停顿周期大约是总周期数的一半,也等于调度停顿周期。正是由于这个原因,内部循环以 2.0c/iter 执行;因为内循环的跳转指令每隔一个周期就是 issued/dispatched。当 LSD 未激活时,微指令可以来自 DSB、MITE 或 MSROM。我们的循环不需要微码辅助,因此 DSB、MITE 或两者中可能存在限制。我们可以进一步调查以确定使用前端性能事件的限制在哪里。我已经这样做了,结果显示大约 80-90% 的微指令来自 DSB。 DSB 本身有很多局限性,循环似乎正在影响其中一个。似乎 DSB 需要 2 个周期来传递以自身为目标的跳转指令。此外,对于整个 IC
范围,由于 MITE-DSB 切换引起的停顿占所有周期的 9%。同样,这些转换的原因是 DSB 本身的限制。请注意,高达 20% 的数据来自 MITE 路径。假设 uops 不超过 MITE 路径的 16B/c 带宽,我认为如果 DSB 不存在,循环将以 1c/iter 执行。
上图还显示了 BPU 错误预测率(每次外循环迭代)。在IvB上,IC
=1-33时为零,IC
=21时除外,IC
=34-45时为0-1,IC
时为1 >46。在 SnB 上,IC
=1-33 为零,否则为 1。
(部分回答/猜测我在哈迪发布详细分析之前没有写完;其中一些是从评论中继续的)
Agner's statement "the loop buffer has no measurable effect in the cases where the uop cache is not a bottleneck..." is wrong? Because this is certainly a measurable effect and the uop cache isn't bottleneck because the cache has ~1.5K capacity.
是的,Agner 称它为环回缓冲区。 他的说法是将 LSD 添加到设计中不会加速任何代码。但是,是的,对于非常紧密的循环来说似乎是错误的,至少对于嵌套循环来说是这样。显然 SnB/IvB 确实需要循环缓冲区来发出或执行 1c/iter 循环。除非微架构瓶颈是在分支后从 uop 缓存中获取 uops,在这种情况下,他的警告涵盖了这一点。
除了 uop 缓存未命中之外,在其他情况下读取 uop 缓存可能成为瓶颈。例如如果 uops 由于对齐效果而不能很好地打包,或者如果它们使用大立即数 and/or 位移需要额外的周期才能从 uop 缓存中读取。有关这些效果的更多详细信息,请参阅 Agner Fog's uarch guide 的 Sandybridge 部分。您假设容量(如果它们完美包装则高达 1.5k uops)是它可能变慢的唯一原因是非常错误的。
顺便说一句,Skylake 的微代码更新完全禁用了 LSD 以修复部分寄存器合并错误,错误 SKL1501,以及除非一个小循环跨越 32B 边界并需要 2 个缓存行,否则实际上效果很小。
但 Agner 列出 JMP rel8/32
并在 HSW/SKL 上将 JCC 吞吐量视为 1-2 个周期,而在 IvB 上仅为 2 个周期。因此,除了 LSD 本身之外,自 IvB 以来,关于采取的分支的一些事情可能已经加速。
可能 CPU 的某些部分不是 LSD,它也有一个特殊情况,用于长 运行 宁小循环,让他们 运行 1在 Haswell 及更高版本上按时钟跳转。我还没有测试什么条件导致 1 vs. 2 循环在 HSW/SKL 上采用分支吞吐量。另请注意,Agner 在错误 SKL150 的微代码更新之前进行了测量。
脚注 1:请参阅
(我之前认为 Coffee Lake 已经重新启用了 LSD,但它似乎没有 - wikichip 明确表示它仍然被禁用,所以我认为这是在纠正一些早期的报告,即它已重新启用。CFL不过,确实修复了 L1TF 和 Meltdown 漏洞,使得针对这些漏洞的软件缓解变得不必要。)