TL;DR:DSB 似乎只能每隔一个周期提供一次内部循环的跳转。DSB-MITE 开关也占执行时间的 9%。
简介 - 第 1 部分:了解 LSD 性能事件
我将首先讨论IvB 和 SnB 微架构上 LSDLSD.UOPS
和LSD.CYCLES_ACTIVE
性能事件发生的时间以及一些特性。一旦我们建立了这个基础,我们就可以回答这个问题。为此,我们可以使用专门设计用于准确确定这些事件何时发生的小段代码。
根据文档:
LSD.UOPS
:LSD 提供的 Uop 数。
LSD.CYCLES_ACTIVE
: LSD 提供的 Cycles Uops,但不是来自解码器。
这些定义很有用,但正如您稍后将看到的,它们不够精确,无法回答您的问题。更好地理解这些事件很重要。此处提供的一些信息并未由英特尔记录,这只是我对经验结果和我所经历的一些相关专利的最佳解释。尽管我无法找到描述 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融合到一个uop中。IvB 和 SnB 上只有一个执行端口可以执行跳转指令。因此,最大吞吐量应为 1c/iter。不过,出于某种原因,IvB 的速度要快 10%。
根据执行 uop 计数不是处理器宽度倍数的循环时性能会降低吗?,即使有可用的问题槽,IvB 和 SnB 中的 LSD 也无法跨循环体边界发出微指令。由于循环包含单个 uop,我们希望 LSD 每个周期发出一个 uop,并且LSD.CYCLES_ACTIVE
应该大约等于周期总数。
在 IvB 上,LSD.UOPS
正如预期的那样。也就是说,LSD 将在每个周期发出一个 uop。请注意,由于循环数等于迭代次数,即等于 uop 的数量,我们可以等效地说 LSD 每次迭代发出一个 uop。本质上,大多数执行的微指令都是从 LSD 发出的。但是,LSD.CYCLES_ACTIVE
大约是周期数的一半。这怎么可能?在这种情况下,不应该只从 LSD 发出一半的微指令吗?我认为这里发生的情况是循环基本上展开了两次,每个循环发出两个微指令。尽管如此,每个周期只能执行一个 uop,但RESOURCE_STALLS.RS
它为零,表明 RS 永远不会被填满。然而,RESOURCE_STALLS.ANY
大约是循环计数的一半。现在把所有这些放在一起,似乎 LSD 实际上每隔一个周期就会发出 2 次微指令,并且每隔一个周期就会达到一些结构限制。CYCLE_ACTIVITY.CYCLES_NO_EXECUTE
确认在任何给定周期内 RS 中始终存在至少一个读取 uop。以下实验将揭示展开发生的条件。
在 SnB 上,LSD.UOPS
显示从 LSD 发出的微指令总数是两倍。还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
,RESOURCE_STALLS.ANY
必须从实测中减去LSD.CYCLES_ACTIVE
。这同样适用于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 上运行的所有基准(包括此处未显示的),这些调整都是准确的。
请注意,RESOURCE_STALLS.RS
SnBRESOURCE_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个uop,但第一个是单周期ALU uop,与跳转uop无关。B3 帮助我们回答以下两个问题:
- 如果跳转的目标不是跳转 uop,那么
LSD.UOPS
和LSD.CYCLES_ACTIVE
仍然会在 SnB 上计数两次吗?
- 如果循环包含 2 个微指令,其中只有一个是跳转,LSD 会展开循环吗?
B3 表明这两个问题的答案都是“否”。
UOPS_ISSUED.STALL_CYCLES
表明如果 LSD 在一个周期内发出两个跳转微指令,它只会停止一个周期。这在 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 个微指令,因为加载 ALU 指令在 RS 中未融合。在之前的基准测试中,没有微融合的微指令,只有宏融合的微指令。这里的目标是了解 LSD 如何处理微融合的微指令。
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 队列发出 uop,前端(即获取和解码单元)甚至IDQ 下游不需要的逻辑都可以断电。在 Nehalem 之前,LSD 是在 IQ 单元中实施的. 从 Haswell 开始,LSD 支持包含来自 MSROM 的微指令的循环。Skylake 处理器中的 LSD 被禁用,因为显然它有问题。
循环通常包含至少一个条件分支。LSD 本质上监控后向条件分支并尝试确定构成循环的微指令序列。如果 LSD 花费太多时间来检测环路,则性能可能会下降并且可能会浪费功率。另一方面,如果 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_4_UOPS
除LSD.CYCLES_ACTIVE
和之外的性能事件BR_MISP_RETIRED.CONDITIONAL
。请注意,由于只有一个分支执行端口,因此每次外循环迭代至少需要 2 个周期(或根据 Agner 的表格,1-2 个周期)。另请参阅:LSD 能否从检测到的循环的下一次迭代中发出 uOP?.
跳转 uops 的总数为:
OC
+ IC
* OC
= 100M/( IC
+3) + IC
*100M/( IC
+3)
= 100M( IC
+1)/( IC
+3)
假设最大跳转uop吞吐量为1个周期,最优执行时间为100M( IC
+1)/( IC
+3)个周期。在 IvB 上,如果我们想要严格,我们可以改为使用 0.9/c 的最大跳转 uop 吞吐量。将其除以内部迭代的数量会很有用:
OPT
= (100M( IC
+1)/( IC
+3)) / (100M IC
/( IC
+3)) =
100M( IC
+1) * ( IC
+3) / ( IC
+3) * 100M IC
=
( IC
+1)/ IC
= 1 + 1 /IC
因此,OPT
对于 > 1,1 < <= 1.5。IC
设计 LSD 的人可以使用它来比较 LSD 的不同设计。我们很快也会使用它。换句话说,当循环总数除以跳跃总数为 1(或 IvB 上为 0.9)时,可以获得最佳性能。
假设两个跳跃的预测是独立的,并且jnz .loop_outer
很容易预测,那么性能取决于 的预测jnz .loop_inner
。在将控制更改为锁定循环之外的微指令的错误预测中,LSD 终止循环并尝试检测另一个循环。LSD 可以表示为具有三种状态的状态机。在一种状态下,LSD 正在寻找循环行为。在第二种状态下,LSD 正在学习循环的边界和迭代次数。在第三种状态下,LSD 正在重放循环。当循环存在时,状态从第三个变为第一个。
正如我们从前一组实验中了解到的,当后端相关的问题停顿时,SnB 上会出现额外的 LSD 事件。所以需要相应地理解这些数字。IC
请注意,上一节中没有测试 =1的情况。将在这里讨论。还记得,在 IvB 和 SnB 上,内部循环可能会展开。外部循环永远不会展开,因为它包含多个微指令。顺便说一句,LSD.CYCLES_4_UOPS
按预期工作(抱歉,这并不奇怪)。
下图显示了原始结果。我仅在 IvB 和 SnB 上分别显示了高达IC
=13 和=9 的结果。IC
我将在下一节讨论更大的值会发生什么。请注意,当分母为零时,无法计算该值,因此不会绘制它。
LSD.UOPS/100M
是从 LSD 发出的 uops 数量与 uops 总数的比率。LSD.UOPS/OC
是每次外部迭代从 LSD 发出的平均微指令数。LSD.UOPS/(OC*IC)
是每次内部迭代从 LSD 发出的平均微指令数。BR_MISP_RETIRED.CONDITIONAL/OC
是每次外部迭代被错误预测的退休条件分支的平均数量,对于 all ,IvB 和 SnB 显然为零IC
。
对于IC
IvB 上的 =1,所有微指令都是从 LSD 发出的。始终不采用内部条件分支。第二张图显示的LSD.CYCLES_4_UOPS/LSD.CYCLES_ACTIVE
指标显示,在 LSD 处于活动状态的所有周期中,LSD 每个周期发出 4 条微指令。我们从之前的实验中了解到,当 LSD 在同一个周期内发出 2 个跳转微指令时,由于某些结构限制,它不能在下一个周期发出跳转微指令,因此会停顿。LSD.CYCLES_ACTIVE/cycles
表明LSD(几乎)每隔一个周期就会停止。我们预计执行外部迭代大约需要 2 个周期,但cycles
表明它需要大约 1.8 个周期。这可能与我们之前看到的 IvB 上的 0.9 跳转 uop 吞吐量有关。
IC
除了两件事之外,SnB 上的情况=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 中确定哪些 uops 以 4 个一组的形式发布,哪些 uops 以小于 4 个的组从 LSD 发布。事实上,我们不知道在存在 LSD 错误预测的情况下如何计算 LSD 事件。潜在的展开增加了另一个层次的复杂性。LSD 事件计数可以被认为是 LSD 发出的有用微指令的上限以及 LSD 发出有用微指令的周期。
随着IC
增加,减少LSD.CYCLES_ACTIVE
和LSD.CYCLES_4_UOPS
减少以及性能缓慢但持续下降(请记住cycles
/( OC
* IC
) 应该与 进行比较OPT
)。就好像最后一次内部循环迭代被错误预测了,但它的错误预测惩罚随着 增加IC
。请注意,BPU 总是正确地预测内循环迭代的次数。
答案
我将讨论 any 会发生什么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 似乎具有攻击性,也许它认为循环更可预测。对于IC
>27,LSD 活动周期计数缓慢增长,这与性能的逐渐提高相关。虽然图中没有显示,如IC
远远超过 64,大多数 uops 将来自 LSD,cycles
/jumps 稳定在 0.9。
范围的结果IC
=13-27 特别有用。问题停顿周期约为总周期计数的一半,也等于调度停顿周期。正是由于这个原因,内部循环以 2.0c/iter 执行;因为内部循环的跳转微指令每隔一个周期就会发出/调度一次。当 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 为 0,但IC
=21 时为 0-1,IC
=34-45时为 0-1, IC
>46 时为 1。在 SnB 上,IC
=1-33 为零,否则为 1。