为什么jnz需要在内循环中完成2个循环

use*_*622 5 x86 micro-optimization microbenchmark micro-architecture

我在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
Run Code Online (Sandbox Code Playgroud)

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
Run Code Online (Sandbox Code Playgroud)

是:

 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%)
Run Code Online (Sandbox Code Playgroud)

perf参考案例的结果:

global _start
_start:
    mov rcx, 100000000
.loop_outer:
    mov rax,    16
    dec rcx
    jnz .loop_outer

    xor edi, edi
    mov eax, 60
    syscall
Run Code Online (Sandbox Code Playgroud)

是:

   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%)
Run Code Online (Sandbox Code Playgroud)

因此原因大多是清楚的:LSD在嵌套的情况下由于某种原因停止工作.减小内环尺寸将略微缓解缓慢,但不是完全缓慢.

搜索英特尔"优化手册",我发现如果循环包含"超过八个采用的分支",LSD将无法工作.这以某种方式解释了这种行为.

Had*_*ais 3

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


简介 - 第 1 部分:了解 LSD 表现事件

我将首先讨论IvB 和 SnB 微架构上的事件LSD.UOPSLSD.CYCLES_ACTIVE性能事件何时发生以及 LSD 的一些特性。一旦我们建立了这个基础,我们就可以回答这个问题了。为此,我们可以使用专门设计的小段代码来准确确定这些事件何时发生。

根据文档:

LSD.UOPS:LSD 交付的 Uop 数。
LSD.CYCLES_ACTIVE:循环 Uops 由 LSD 提供,但不是来自解码器。

这些定义很有用,但是,正如您稍后将看到的,它们不够精确,无法回答您的问题。更好地了解这些事件非常重要。这里提供的一些信息并未由英特尔记录,这只是我对经验结果和我经历过的一些相关专利的最佳解释。尽管我无法找到描述 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
Run Code Online (Sandbox Code Playgroud)

循环体中的两条指令都被 mac 融合成一个 uop。IvB和SnB上只有一个执行端口可以执行跳转指令。因此,最大吞吐量应为1c/iter。不过,出于某种原因,IvB 速度快了 10%。

根据执行微指令计数不是处理器宽度倍数的循环时性能是否会降低?,即使有可用的发出槽,IvB 和 SnB 中的 LSD 也无法跨循环体边界发出微指令。由于循环包含单个微指令,我们预计 LSD 将在每个周期发出单个微指令,并且该数量LSD.CYCLES_ACTIVE应大约等于周期总数。

在 IvB 上,LSD.UOPS正如预期的那样。也就是说,LSD 每个周期将发出一个 uop。请注意,由于循环数等于迭代数,而迭代数又等于微指令的数量,因此我们可以等效地说,LSD 每次迭代发出一个微指令。本质上,大多数执行的微指令都是从 LSD 发出的。然而,LSD.CYCLES_ACTIVE大约是周期数的一半。这怎么可能?这样的话,LSD 不应该只发出总数 uop 的一半吗?我认为这里发生的情况是,循环基本上展开了两次,并且每个周期发出了两个微指令。尽管如此,每个周期只能执行一个微指令,但RESOURCE_STALLS.RS其值为零,这表明 RS 永远不会变满。然而,RESOURCE_STALLS.ANY大约是周期数的一半。现在将所有这些放在一起,似乎 LSD 实际上每隔一个周期发出 2 个 uop ,并且每隔一个周期就会达到一些结构限制。CYCLE_ACTIVITY.CYCLES_NO_EXECUTE确认在任何给定周期,RS 中始终至少有一个读 uop。以下实验将揭示展开发生的条件。

在 SnB 上,LSD.UOPS显示 LSD 发出的 uop 总数是两倍。也LSD.CYCLES_ACTIVE表明 LSD 在大部分时间都处于活跃状态。CYCLE_ACTIVITY.CYCLES_NO_EXECUTEUOPS_ISSUED.STALL_CYCLESIvB 上的一样。以下实验有助于理解正在发生的情况。看起来测量值LSD.CYCLES_ACTIVE等于实际值LSD.CYCLES_ACTIVE+ RESOURCE_STALLS.ANY。因此,要得到真实的LSD.CYCLES_ACTIVERESOURCE_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 上运行的所有基准测试(包括此处未显示的基准测试),这些调整都是准确的。

请注意, SnB 上的RESOURCE_STALLS.RSRESOURCE_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
Run Code Online (Sandbox Code Playgroud)

在B2中,每次迭代有2个uop,并且都是跳跃。第一个从未被采用,因此仍然只有一个循环。我们预计它的运行速度为 2c/iter,事实确实如此。LSD.UOPS显示大多数 uop 是由 LSD 发出的,但LSD.CYCLES_ACTIVE显示 LSD 仅在一半的时间内处于活动状态。这意味着循环未展开。因此,似乎只有当循环中有单个微指令时才会发生展开。

; 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
Run Code Online (Sandbox Code Playgroud)

这里也有2个uop,但第一个是单周期ALU uop,与跳转uop无关。B3帮助我们回答以下两个问题:

  • 如果跳转的目标不是跳转uop,则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
Run Code Online (Sandbox Code Playgroud)

B4 还有一个额外的变化:它在融合域中包含 2 个微指令,但在融合域中包含 3 个微指令,因为加载 ALU 指令在 RS 中未融合。在之前的基准测试中,没有微融合微指令,只有宏融合微指令。这里的目标是了解 LSD 如何处理微融合微指令。

LSD.UOPS显示加载 ALU 指令的两个微指令已经消耗了单个发出槽(融合跳转微指令仅消耗单个槽)。此外,由于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
Run Code Online (Sandbox Code Playgroud)

B5 是我们需要的最后一个基准。它与 B2 类似,包含两个分支微指令。然而,B5 中的跳转 uop 之一是向前无条件跳转。结果与 B2 相同,表明跳转 uop 是否有条件并不重要。如果第一个跳转 uop 是有条件的而第二个跳转 uop 不是有条件的,情况也是如此。

简介 - 第 2 部分:LSD 中的分支预测

LSD 是在 uop 队列 (IDQ) 中实现的机制,可以提高性能并降低功耗(从而减少热量排放)。它可以提高性能,因为前端存在的一些限制可能在 uop 队列中得到缓解。特别是,在SnB和IvB上,MITE和DSB路径的最大吞吐量均为4uops/c,但就字节而言,分别为16B/c和32B/c。uop队列带宽也是4uops/c,但对字节数没有限制。只要 LSD 从微指令队列发出微指令,前端(即获取和解码单元)甚至IDQ 下游不需要的逻辑都可以断电。在 Nehalem 之前,LSD 是在 IQ 部门实施的。从 Haswell 开始,LSD 支持包含来自 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
Run Code Online (Sandbox Code Playgroud)

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

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

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

与问题中的代码相比,多了一条指令 ,mov rbx, 1因此外循环中的 uops 总数恰好为 4 uops。这使我们能够利用LSD.CYCLES_4_UOPS除了LSD.CYCLES_ACTIVE和之外的性能事件BR_MISP_RETIRED.CONDITIONAL。请注意,由于只有一个分支执行端口,因此每次外循环迭代至少需要 2 个周期(或者根据 Agner 的表,1-2 个周期)。另请参阅:LSD 能否从检测到的循环的下一次迭代中发出 uOP?

跳转uop的总数为:

OC+ IC* OC= 100M/( + 3 IC) + IC*100M/(+3 )      = 100M( +1)/( +3)IC
ICIC

假设每个周期最大跳转uop吞吐量为1,则最优执行时间为100M( IC+1)/( IC+3)个周期。在 IvB 上,如果我们想严格一点,我们可以使用最大跳转 uop 吞吐量 0.9/c。将其除以内部迭代次数会很有用:

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

因此,1 < OPT<= 1.5 对于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按预期工作(抱歉,没有惊喜)。

下图显示了原始结果。IC我仅显示了 IvB 和 SnB 上分别达到=13 和=9的结果IC。我将在下一节中讨论较大值会发生什么。请注意,当分母为零时,无法计算该值,因此不会绘制该值。

微操作指标 周期指标

LSD.UOPS/100M是LSD发出的uop数与总uop数的比率。LSD.UOPS/OC是每次外部迭代从 LSD 发出的平均微指令数。LSD.UOPS/(OC*IC)是每次内部迭代从 LSD 发出的平均微指令数。BR_MISP_RETIRED.CONDITIONAL/OC是每次外部迭代错误预测的已退休条件分支的平均数量,对于所有 ,该值在 IvB 和 SnB 上显然为零IC

对于ICIvB =1,所有 uop 均由 LSD 发出。内部条件分支始终不被采用。第二张图中显示的指标LSD.CYCLES_4_UOPS/LSD.CYCLES_ACTIVE显示,在 LSD 处于活动状态的所有周期中,LSD 每个周期发出 4 个微指令。我们从之前的实验中了解到,当LSD在同一周期内发出2个跳跃uop时,由于某些结构限制,它无法在下一个周期发出跳跃uop,因此会停滞。LSD.CYCLES_ACTIVE/cycles显示 LSD(几乎)每隔一个周期就会停止一次。我们预计执行一次外迭代大约需要 2 个周期,但cycles结果显示大约需要 1.8 个周期。这可能与我们之前看到的 IvB 上的 0.9 跳转 uop 吞吐量有关。

SnB 上 =1 的情况IC类似,但有两点不同。首先,外层循环实际上需要 2 个周期(如预期),而不是 1.8 个周期。其次,所有三个 LSD 事件计数都是预期的两倍。可以按照上一节中的讨论进行调整。

IC当>1时,分支预测特别有趣。我们来IC详细分析=2的情况。LSD.CYCLES_ACTIVELSD.CYCLES_4_UOPS表明在大约 32% 的所有周期中,LSD 处于活动状态,并且在其中 50% 的周期中,LSD 每个周期发出 4 uop。因此,要么存在错误预测,要么LSD在循环检测状态或学习状态中花费了大量时间。尽管如此,cycles/( OC* IC)约为1.6,或者换句话说,cycles/jumps为1.07,这接近最佳性能。很难弄清楚哪些微指令是从 LSD 以 4 个一组的形式发出的,哪些微指令是从 LSD 中以小于 4 个的组发出的。事实上,我们不知道在存在 LSD 错误预测的情况下如何计算 LSD 事件。潜在的展开又增加了复杂性。LSD事件计数可以被认为是LSD发出的有用uop的上限以及LSD发出有用uop的周期。

随着IC增加、LSD.CYCLES_ACTIVELSD.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
Run Code Online (Sandbox Code Playgroud)

这与问题中的代码本质上相同。唯一的区别是调整外部迭代的数量以保持相同数量的动态微指令。请注意,在这种情况下这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,大部分 uop 将来自 LSD,并且cycles/jumps 稳定在 0.9。

=13-27范围内的结果IC特别有用。发出停顿周期大约是总周期计数的一半,也等于调度停顿周期。正是由于这个原因,内部循环的执行速度为 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 上,=1-33 时为零, =21IC时除外; =34-45 时为 0-1;>46 时恰好为 1。在 SnB 上,=1-33 时为零,否则为 1。ICICICIC


归档时间:

查看次数:

175 次

最近记录:

7 年,4 月 前