TL;DR:DSB 似乎只能每隔一个周期提供一次内部循环的跳转。 DSB-MITE 开关也占执行时间的 9%。
简介 - 第 1 部分:了解 LSD 性能事件
我将首先讨论LSD.UOPS 和LSD.CYCLES_ACTIVE 性能事件何时发生以及 IvB 和 SnB 微架构上 LSD 的一些特性。一旦我们建立了这个基础,我们就可以回答这个问题。为此,我们可以使用专门设计用于准确确定这些事件何时发生的小段代码。
根据文档:
LSD.UOPS:LSD 交付的 Uop 数量。
LSD.CYCLES_ACTIVE:LSD 交付但未交付的周期 Uop
来自解码器。
这些定义很有用,但正如您稍后会看到的,它们不够精确,无法回答您的问题。更好地理解这些事件很重要。这里提供的一些信息没有被英特尔记录,这只是我对经验结果和我所经历的一些相关专利的最佳解释。虽然我找不到描述 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%。
根据Is performance reduced when executing loops whose uop count is not a multiple of processor width?,即使有可用的问题槽,IvB 和 SnB 中的 LSD 也无法跨循环体边界发出微指令。由于循环包含单个微指令,我们预计 LSD 将在每个周期发出一个微指令,并且LSD.CYCLES_ACTIVE 应该大约等于周期总数。
在 IvB 上,LSD.UOPS 符合预期。也就是说,LSD 将在每个周期发出一个 uop。请注意,由于循环数等于迭代次数,即等于 uop 的数量,我们可以等效地说 LSD 每次迭代发出一个 uop。本质上,大多数执行的微指令都是从 LSD 发出的。但是,LSD.CYCLES_ACTIVE 大约是周期数的一半。这怎么可能?在这种情况下,不应该只从 LSD 发出一半的微指令吗?我认为这里发生的情况是循环基本上展开了两次,每个循环发出两个微指令。尽管如此,每个周期只能执行一个微指令,而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,必须从测量的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.UOPSreal = 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 表明大多数微指令是从 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 帮助我们回答了以下两个问题:
- 如果跳转的目标不是跳转微指令,
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 队列发出 uops,前端(即获取和解码单元)甚至 unneeded logic downstream from the IDQ 都可以断电。在 Nehalem 之前,LSD was implemented in the IQ unit。从 Haswell 开始,LSD 支持loops that contain uops from the MSROM。 Skylake 处理器中的 LSD 被禁用,因为显然它有问题。
循环通常包含至少一个条件分支。 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_ACTIVE 和BR_MISP_RETIRED.CONDITIONAL,这使我们能够使用LSD.CYCLES_4_UOPS 性能事件。请注意,由于只有一个分支执行端口,因此每次外循环迭代至少需要 2 个周期(或根据 Agner 的表格,1-2 个周期)。另见:Can the LSD issue uOPs from the next iteration of the detected loop?。
跳转的uop总数为:
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)) / (100MIC/(IC+3)) =
100M(IC+1) * (IC+3) / (IC+3) * 100MIC =
(IC+1)/IC = 1 + 1/IC
因此,1 OPT IC > 1. 设计 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 和 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,所有微指令都是从 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 吞吐量有关。
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 中确定哪些 uops 以 4 个一组的形式发布,哪些 uops 以小于 4 个的组从 LSD 发布。事实上,我们不知道在存在 LSD 错误预测的情况下如何计算 LSD 事件。潜在的展开增加了另一个层次的复杂性。 LSD事件计数可以被认为是LSD发出的有用微指令的上限以及LSD发出有用微指令的周期。
随着IC 的增加,LSD.CYCLES_ACTIVE 和LSD.CYCLES_4_UOPS 都会下降,性能会缓慢但持续下降(请记住,cycles/(OC*IC) 应该与OPT 进行比较)。就好像最后一次内部循环迭代被错误预测了,但它的错误预测惩罚随着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 认为由于错误预测最后一次内部迭代而导致的性能影响大于某个阈值,它决定永远不锁定循环并记住它的决定。当ICIC>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 时为零,IC=21 时除外,IC=34-45 时为 0-1,IC>46 时为 1。在 SnB 上,IC=1-33 为零,否则为 1。