【问题标题】:Why jnz requires 2 cycles to complete in an inner loop为什么 jnz 需要 2 个周期才能在内循环中完成
【发布时间】:2019-06-06 23:09:13
【问题描述】:

我在常春藤桥上。我发现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的指令表中搜索,条件跳转有1-2个“倒数吞吐量”,并带有注释“如果没有跳转则快”。

此时我开始相信上述行为是可以预料的。但是为什么外循环中的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 在嵌套情况下由于某种原因停止工作。减小内部循环大小会稍微减轻缓慢,但不能完全减轻。

搜索英特尔“优化手册”,我发现如果循环包含“超过八个已采取的分支”,LSD 将不起作用。这以某种方式解释了这种行为。

【问题讨论】:

  • 16 次迭代应该足够少,以至于内部循环的循环退出可以正确预测(您可能会看到更慢的时间),但无论如何您都应该检查。 (大约 23 次迭代是我上次测试时它停止在 Skylake 上正确预测的时间)。长时间运行的紧密循环是一种特殊情况,由前端使用循环缓冲区专门处理。这可能会破坏循环缓冲区(LSD);检查 lsd.uopsuops_issued.any 的计数器。 (我不认为 LSD 可以处理嵌套循环,所以充其量所有的内循环微指令都来自 LSD,但可能更少)
  • 也值得尝试将外部循环对齐 32。这应该将整个东西(内部+外部)放在同一个 uop-cache 行中。解码器不会在 IvB 上背靠背 dec/jnz 进行宏融合(或者实际上,如果它们在最多 4 个 uops 的同一组中击中解码器),仅在 HSW 和更高版本上,所以请记住,您的外部循环可能对 dec 和 jnz 有单独的 uops。不过,这不是你所看到的任何东西的直接原因。顺便说一句,您如何衡量存在内循环的外循环 JNZ 的成本?还是您真的是指“在一个长时间运行的循环中”,没有嵌套 1c/iter?
  • @PeterCordes 谢谢,你是对的,LSD 是原因。请参阅我的编辑。对齐没有区别,分支预测在这两种情况下都能完美运行。如果你写这些 cmets 作为答案,我会接受。
  • @PeterCordes 我还有一个疑问:LSD 和 Agner 书中的“环回缓冲区”是一回事吗?看起来是一样的,但如果是这样,Agner 的说法“在 uop 缓存不是瓶颈的情况下,循环缓冲区没有可测量的效果……”是错误的吗?因为这肯定是一个可测量的效果,而且 uop 缓存不是瓶颈,因为缓存的容量约为 1.5K。
  • 是的,Agner 将其称为环回缓冲区。他的声明是在设计中添加 LSD 不会加速任何代码。但是,是的,对于非常紧凑的循环来说似乎是错误的,显然 SnB/IvB 确实需要循环缓冲区来发出或执行 1c/iter 循环。除非微架构瓶颈是在分支后从 uop 缓存中获取 uops,在这种情况下,他的警告涵盖了这一点。

标签: x86 micro-optimization microbenchmark micro-architecture


【解决方案1】:

(部分答案/猜测在哈迪发布详细分析之前我还没有写完;其中一些正在从 cmets 继续)

Agner 的说法“在 uop 缓存不是瓶颈的情况下,循环缓冲区没有可测量的效果……”是错误的吗?因为这肯定是一个可测量的效果,而且 uop 缓存不是瓶颈,因为缓存的容量约为 1.5K。

是的,Agner 将其称为环回缓冲区。 他的说法是在设计中添加 LSD 不会加速任何代码。但是,是的,对于非常紧凑的循环来说,这似乎是错误的,至少对于嵌套循环来说是这样。显然 SnB/IvB 确实需要循环缓冲区来发出或执行 1c/iter 循环。除非微架构瓶颈是在分支后从 uop 缓存中获取 uops,在这种情况下,他的警告涵盖了这一点。

除了 uop-cache 未命中之外,读取 uop 缓存可能会成为瓶颈。例如如果 uops 由于对齐效应而没有很好地打包,或者如果它们使用大的立即数和/或需要额外周期才能从 uop 缓存中读取的位移。有关这些效果的更多详细信息,请参阅Agner Fog's uarch guide 的 Sandybridge 部分。您认为容量(如果包装完美,最高可达 1.5k 微指令)是它可能变慢的唯一原因的假设是非常错误的。

顺便说一句,Skylake 的微码更新完全禁用了 LSD 以修复部分寄存器合并错误,勘误 SKL1501,实际上并没有什么效果,除了当一个小循环跨越 32B 边界并需要 2 个缓存行时。

但 Agner 列出了 JMP rel8/32,并在 HSW/SKL 上将 JCC 吞吐量视为 1-2 个周期,而在 IvB 上仅为 2 个周期。因此,除了 LSD 本身之外,自 IvB 以来,关于所采取的分支的某些事情可能已经加速。

除了 LSD 之外,CPU 的某些部分可能还有一个特殊情况,用于长时间运行的微小循环,让它们在 Haswell 及更高版本上每时钟运行 1 次跳转。我还没有测试什么条件会导致 HSW/SKL 上的 1 与 2 周期采用分支吞吐量。另请注意,Agner 在错误 SKL150 的微码更新之前测量。


脚注 1:请参阅 How exactly do partial registers on Haswell/Skylake perform? Writing AL seems to have a false dependency on RAX, and AH is inconsistent,并注意 SKX 和 Kaby Lake 附带的微码已包含此内容。它终于在 CannonLake / Ice Lake 等 CPU 中重新启用,修复了有缺陷的硬连线逻辑,因此可以安全地再次启用 LSD。

(我之前认为 Coffee Lake 已重新启用 LSD,但似乎没有 - wikichip 明确表示它仍处于禁用状态,所以我认为这纠正了一些早期的报告,即它已重新启用。CFL 确实修复了 L1TF 和但是,崩溃漏洞使得软件缓解对于这些漏洞来说是不必要的。)

【讨论】:

  • 赞成。你的回答和哈迪的都很好,在你发帖之前我已经接受了哈迪的,对不起。我已经重读了 Agner 书中的 uop 缓存部分,但我对 uop 缓存组织更加困惑。 Agner 表明:“如果同一段代码有多个跳转条目,则它可以在 uop 缓存中有多个条目”。一段代码怎么可能有多个条目?在我的代码中,inner_loop 的计数器是 16,我之前以为它只有一个 uop 条目——宏融合的jnz。 Agner 是否意味着它有 16 个条目?
  • @user10865622:不,这个循环有相同的入口点跳转到 16 次。想象一个循环体,在整个循环体的中间有一个第一次迭代入口点,比如跳转到循环条件策略,gcc -Os 用于可能需要运行 0 次的循环。 (Why are loops always compiled into "do...while" style (tail jump)?)。 CPU 最终可能会从循环顶部重新解码并创建一个新的 uop 缓存行,可能不会驱逐原始缓存行(立即/根本)。
  • @user10865622:我认为 Hadi 更直接地回答了具体问题,而我的则是解决来自 cmets 的额外问题,因此您应该保留 Hadi 的接受复选标记。 (尽管如此,您确实可以移动它,但是在某些情况下无法接受多个有用的方法。)
  • 如果我正确理解您的评论,CPU 有能力区分非分支之后的指令和分支之后的指令。在前一种情况下,将创建一个新条目,而在后一种情况下,CPU 将使用现有条目。我说的对吗?
  • 我明白了,谢谢。我从错误的角度概括了您的 while 循环示例。更多的是uop缓存行的对齐问题,实际实现可能比较复杂。
【解决方案2】:

TL;DR:DSB 似乎只能每隔一个周期提供一次内部循环的跳转。 DSB-MITE 开关也占执行时间的 9%。


简介 - 第 1 部分:了解 LSD 性能事件

我将首先讨论LSD.UOPSLSD.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_EXECUTEUOPS_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.RSRESOURCE_STALLS.ANY 就像 IvB。因此,就这个特定的基准而言,LSD 在 IvB 和 SnB 上的工作方式似乎相同,只是事件 LSD.UOPSLSD.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.UOPSLSD.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

OCIC 分别表示外部迭代次数和内部迭代次数。它们的关系如下:

OC = 100000000/(IC+3)       其中IC > 0

对于任何给定的IC,退役的微指令总数是相同的。此外,融合域中的微指令数等于未融合域中的微指令数。这很好,因为它确实简化了分析,并允许我们在 IC 的不同值之间进行公平的性能比较。

与问题中的代码相比,多了一条指令mov rbx, 1,这样外循环中的微指令总数正好是4微指令。除了LSD.CYCLES_ACTIVEBR_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_ACTIVELSD.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_ACTIVELSD.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。

【讨论】:

  • 在 IC=1 的情况下,我们可能无法获得宏融合,因为只有 HSW 和更高版本才能在一个解码组中进行 2 次宏融合。但是如果内循环分支至少被采用一次,那么当内循环最终退出时,可能会重新解码外循环 dec/jnz,而不是保存未执行路径的解码结果。这仍然不能解释 IC=1 如何导致每个周期超过 1 次跳跃,但这是一个潜在的质量差异。
  • @PeterCordes 我很惊讶地看到 B4 中的存储 uop 显然使用了 2 个问题槽,这意味着它没有层压,即使它没有使用索引寻址模式。你知道这是否由英特尔记录?我错过了什么吗?
  • 具有立即数和相对 RIP 操作数的指令一开始就永远不能进行微融合,即使在解码器中也是如此。所以没有分层,我认为这在英特尔手册的其他地方都有记录。这适用于mov-stores 和testcmp [rel foo], imm。有关示例,请参阅Micro fusion and addressing modes。 IIRC,add [rel foo], imm 的负载+操作部分也不能微熔。
猜你喜欢
  • 2023-03-11
  • 1970-01-01
  • 2023-03-31
  • 2019-12-19
  • 1970-01-01
  • 2016-12-11
  • 2015-12-23
  • 2019-05-30
  • 2013-02-06
相关资源
最近更新 更多