在执行uop数量不是处理器宽度倍数的循环时性能是否降低?

我想知道在近期的x86处理器上,各种尺寸的循环如何作为uops数量的函数。

彼得·科尔德斯(Peter Cordes)的一句话引起了另外一个问题 :

我还发现,如果循环不是4个uops的倍数,那么循环缓冲区中的uop带宽不是每个周期4个常量。 (即它是abc,abc,…;不是abca,bcab,…)。 Agner Fog的微文档不幸的是在循环缓冲区的限制上并不清楚。

这个问题是关于循环是否需要N个uop的倍数来执行最大uop吞吐量,其中N是处理器的宽度。 (即4个用于最近的英特尔处理器)。 谈论“宽度”并计数微波时,有许多复杂的因素,但我最想忽略这些因素。 特别是,假设没有微观或macros观融合。

彼得给出了下面的例子,在它的内部有7个uops循环:

一个7-uop循环将发出4 | 3 | 4 | 3 |组…我还没有testing过较大的循环(不适合在循环缓冲区中),以查看下一个指令是否可能迭代在同一组中发出,但是我认为不是。

更一般地说,声明是每个迭代循环的x uops在它的体内至less需要ceil(x / 4)迭代,而不是简单的x / 4

这对一些或所有最近的x86兼容处理器是否正确?

我使用Linux perf进行了一些调查,以帮助在我的Skylake i7-6700HQ机箱上回答这个问题,并且Haswell结果已经由另一个用户友好地提供。 下面的分析适用于Skylake,但接下来是与Haswell的比较。

其他架构可能会有所不同0 – 我们需要更多的帮助testing来确认! 就此而言,我欢迎来自其他CPU体系结构的任何人提供的额外结果( 可用来源 )。 将其添加到此答案中,或者使用其他调查结果创build答案。

这个问题主要涉及到前端,因为在最近的架构中,它是每个周期施加四个熔融域微分的硬限制的前端。

环路性能规则摘要

首先,我将在处理小循环时根据一些“性能规则”来总结结果。 还有很多其他的performance规则 – 它们是互补的(也就是说,你可能不会违背另一条规则来满足这些规则)。

首先,计算你的循环中的macros电路的数量。 您可以使用Agner的指令表直接查看每条指令,除了ALU uop和紧随其后的分支通常会融合成一个单一的uop。 然后根据这个数字:

  • 如果计数是4的倍数,那么这很好:这些循环以最佳方式执行。
  • 如果计数是偶数且小于32,那么你是好的,除非是10,在这种情况下,如果可以的话,你应该展开到另一个偶数。
  • 对于奇数,如果可以的话,你应该试着展开一个小于32的偶数或4的倍数。
  • 对于大于32 uops但小于64的循环,如果它不是4的倍数,则可能需要展开:超过64个uops,您将在Sklyake上获得任何值的有效性能,而Haswell上的几乎所有值有一些偏差,可能alignment相关)。 这些循环的低效率仍然相对较小:最多要避免的值是4N + 1计数,其次是4N + 2计数。

调查结果摘要

对于从uopcaching中提供的代码,没有明显的4倍数效应。 任何数量的微操作循环都可以在每周期4个熔融域微操作的吞吐量下执行。

对于由传统解码器处理的代码,情况正好相反:循环执行时间限于整数个循环,因此不是4个uops的倍数的循环不能达到4个uops /循环,因为它们浪费了一些问题/执行时隙。

对于从环stream检测器(LSD)发出的代码,情况是这两种情况的混合,并在下面更详细地解释。 通常情况下,小于32个uops的循环以及偶数个uops的执行效果最佳,而奇数大小的循环则不会,而较大的循环则需要4个uop的数量以最佳方式执行。 下面的细节。

细节

任何熟悉的x86-64架构的人都知道,在任何时候,根据代码大小和其他因素,前端的提取和解码部分可能以几种不同的模式工作。 事实certificate,这些不同的模式在循环大小方面都有不同的行为。 我将分别介绍他们跟随。

遗产解码器

传统解码器 1是当代码不适合uop高速caching机制(LSD或DSB)时使用的完整的机器代码到uops解码器。 这将发生的主要原因是如果代码工作集大于uopcaching(在理想情况下大约~1500微微秒,在实践中更less)。 但是,对于这个testing,我们将利用这样一个事实,即如果alignment的32字节块包含超过18条指令,那么传统解码器也将被使用3

为了testing传统的解码器行为,我们使用如下所示的循环:

 short_nop: mov rax, 100_000_000 ALIGN 32 .top: dec rax nop ... jnz .top ret 

基本上,一个细微的循环,直到rax倒数为零。 所有的指令都是一个单一的uop 4nop指令的数目是变化的(在图中所示的位置)来testing不同大小的循环(所以4-uop循环将有2个nop ,加上两个循环控制指令)。 没有macros观融合,因为我们总是把decjnz分开,至less有一个nop ,也没有微融合。 最后,没有内存访问(在暗示的icache访问之外)。

请注意,这个循环非常密集 – 每个指令大约1字节(因为nop指令每个都是1个字节) – 所以我们只要在循环中命令19条指令,就会在32B块条件中触发> 18条指令。 基于检查性能lsd.uopsidq.mite_uops ,这正是我们所看到的:基本上100%的指令从LSD 5直到并包括18个uop循环,但是在19个uops以上,100%来自传统的解码器。

在任何情况下,下面是从3到99的所有循环大小的循环/迭代6

Cyles /迭代给定大小的循环

蓝点是适合LSD的循环,并显示出一些复杂的行为。 我们稍后再看。

红点(从19 uops /迭代开始)由传统解码器处理,并显示出非常可预测的模式:

  • 所有使用N uops的循环都采用完全ceiling(N/4)迭代

因此,至less对于传统的解码器来说,Peter的观察正是在Skylake上进行的:具有4个uops倍数的循环可以在4的IPC上执行,但是任何其他数量的uops将会浪费1,2或3个执行时隙4N+3 4N+2 4N+1条指令)。

我不清楚为什么会发生这种情况。 尽pipe如果考虑解码发生在连续的16B块中,并且因此在4个uops /周期的解码速率循环中,不是4的倍数总是会在周期中具有一些尾随的(浪费的)时隙,但是遇到jnz指令。 然而,实际的读取和解码单元是由预解码和解码阶段组成的,其间有一个队列。 预解码阶段实际上具有6条指令的吞吐量,但是仅解码到每个周期的16字节边界的末尾。 这似乎意味着循环结束时出现的气泡可以被预解码器 – >解码队列吸收,因为预解码器的平均吞吐量高于4。

所以我不能完全解释这个基于我的预解码器如何工作的理解。 在解码或预解码中可能存在一些额外的限制,以防止非积分循环计数。 例如,即使跳转之后的指令在预解码队列中可用,传统解码器也许不能解码跳跃两边的指令。 也许这与处理macros观融合的需要有关。

上面的testing显示了循环顶部在32字节边界上alignment的行为。 下面是同一个图表,但增加了一个系列,它显示了当循环顶部向上移动2个字节(即现在在32N + 30边界处未alignment)时的效果:

遗失的解码器周期/迭代时失准

大多数循环大小现在遭受1或2个周期的惩罚。 当考虑解码16B边界和每个周期4个指令的解码时,1惩罚的情况是有意义的,并且2个循环的惩罚情况出现在由于某种原因DSB被用于循环中的1个指令的循环中(可能是出现的dec指令在其自己的32字节块中),并且产生了一些DSB < – > MITE切换惩罚。

在某些情况下,错位不会因最后alignment循环结束而受到影响。 我testing了错位,它以相同的方式持续到200个循环。 如果以面值的forms描述预解码器,那么看起来像上面那样,它们应该能够隐藏一个未alignment的读取泡泡,但是它不会发生(也许这个队列不够大)。

DSB(Uop Cache)

uopcaching(英特尔喜欢称之为DSB)能够caching大部分适量的指令循环。 在一个典型的程序中,你会希望你的大部分指令都是从这个caching中提供的7

我们可以重复上面的testing,但现在从uopcaching中提供uops。 这是一个简单的问题,将我们的nops的大小增加到2个字节,所以我们不再达到18个指令的限制。 我们在循环中使用2字节的nop xchg ax, ax

 long_nop_test: mov rax, iters ALIGN 32 .top: dec eax xchg ax, ax ; this is a 2-byte nop ... xchg ax, ax jnz .top ret 

在这里,结果非常简单。 对于从DSB输出的所有testing的环路大小,所需的周期数是N/4 ,即,即使没有4个uop的倍数,也以最大理论吞吐量执行环路。 因此,一般来说,在Skylake上,DSB提供的中等大小的循环不需要担心确保uop数量达到某个特定的倍数。

这里是一个图表,以1000个循环。 如果你眯起眼睛,你可以看到64-uops(当循环在LSD中)之前的次优行为。 在那之后,这是一个直截了当的方式,4 IPC全程到1000 uops(大约900的blip可能是由于我的盒子上的负载):

为DSB提供的循环计数循环次数

接下来我们看一下循环的性能,这个循环足够小以适应uopcaching。

LSD(循环蒸汽探测器)

重要提示:英特尔显然已经通过微码更新和Skylake-X开箱, 禁用了Skylake(SKL150纠错)和Kaby Lake(KBL095,KBW095纠错)芯片上的LSD,因为超线程与LSD。 对于那些芯片,下面的图表可能不会有有趣的地区多达64个微博; 相比之下,它会看起来像64区域之后的地区一样。

循环stream检测器可以caching多达64个uops的小循环(在Skylake上)。 在英特尔最近的文档中,它被定位为比性能特征更节能的机制 – 尽pipe使用LSD肯定没有提到性能下降。

运行这个适合LSD的循环大小,我们得到以下周期/迭代行为:

驻留环路的LSD驻留循环

这里的红线是从LSD传送的uops的百分比。 对于5到56微微的所有环路尺寸,它的平坦度为100%。

对于3和4个uop循环,我们有不寻常的行为,分别有16%和25%的uops从传统解码器传送。 咦? 幸运的是,这似乎并没有影响循环吞吐量,因为两种情况都能达到1循环/循环的最大吞吐量 – 尽pipe人们可能会期望一些MITE < – > LSD转换惩罚。

在57和62微微码的循环大小之间,从LSD传送的微码数量显示出一些奇怪的行为 – 大约70%的微码是从LSD传送的,其余的来自DSB。 Skylake名义上有一个64位的LSD,所以这是在超出LSD大小之前的一种转换 – 也许在IDQ内部有一些内部alignment(LSD被实现),这只会导致部分命中LSD在这个阶段。 这个阶段很短,而且在性能方面,似乎大部分是在其之前的全部LSD性能和在其之后的全部DSB性能的线性组合。

让我们来看看5和56之间的结果的主体。 我们看到三个不同的地区:

从3到10个循环循环:在这里,行为是复杂的。 这是我们看到循环计数的唯一区域,在单个循环迭代中不能用静态行为来解释8 。 范围很短,很难说是否有模式。 4个,6个和8个微处理器的循环全部以N/4个周期(与下一个区域相同的模式)执行最佳化。

另一方面,10个循环的循环每次迭代执行2.66个循环,使其成为唯一的循环大小,直到达到34个或更高的循环大小(除了26处的exception值外) 。 这对应于重复的uop / cycle执行率为4,4,4,3的情况。 对于一个5 uops的循环,你每次迭代得到1.33个周期,非常接近,但与1.25的理想不一样。 这对应于执行率为4, 4, 4, 4, 3

我不能真正解释这些结果,而不是说有复杂的事情发生,也许是不正确的。 结果从运行到运行都是可重复的,并且可以对变化进行稳健的修改,比如换出一个实际上做了像mov ecx, 123的指令的nop。

从11圈到32圈:我们看到了一个阶梯式的模式,但是有两个阶段。 基本上所有具有偶数个微操作的循环都能以最佳的方式执行 – 也就是说,只需要N/4个循环。 具有奇数个uops的循环浪费了一个“发行槽”,并且采取与具有一个以上uops的循环相同的循环数(即,17个uop循环与18个uop循环采用相同的4.5个循环)。 所以在这里我们的行为比ceiling(N/4)好很多,我们有第一个证据表明Skylake至less可以在非整数个周期内执行循环。

唯一的exception值是N = 25和N = 26,都比预期的长1.5%。 它很小,但可重现,并强大的function在文件中移动。 这太小了,不能用每迭代效应来解释,除非它有一个巨大的时期,所以这可能是别的。

这里的总体行为是完全一致的(在25/26exception之外),硬件展开为2。

从33到64个循环的循环:我们再次看到一个阶梯模式,但是有一个4的时间段,并且比最高达32个的情况下的平均性能更差。 行为恰恰是ceiling(N/4) – 也就是说,与传统解码器的情况相同。 因此,对于32到64微微的环路, 对于这个特定的限制 ,LSD在传统解码器方面没有明显的优势, 就前端吞吐量而言 。 当然,LSD还有很多其他的方法,比如更复杂或更长的指令,它可以避免许多潜在的解码瓶颈,并且节省了功耗等等。

所有这些都是令人惊讶的,因为这意味着从uopcaching提供的循环通常在前端执行比从LSD发送的循环要好 ,尽pipeLSD通常被定位为比DSB更好的uops源(例如,作为build议的一部分,试图保持循环足够小,以适应LSD)。

下面是查看相同数据的另一种方法 – 就给定计数的效率损失而言,相对于每循环4个微处理器的理论最大吞吐量。 效率达到10%意味着您只需要从简单的N/4公式计算出的吞吐量的90%。

这里的总体行为与没有进行任何展开的硬件是一致的,这是有意义的,因为超过32个uops的循环根本不能在64个uops的缓冲区中展开。

效率损失的循环大小

上面讨论的三个区域的颜色是不同的,至less有相互竞争的效果是可见的:

  1. 在其他条件相同的情况下,所涉及的微博数量越大,效率就越低。 命中是一个固定的成本,每次迭代只有一次,所以较大的循环支付较小的相对成本。

  2. 当你进入33 + UOP区域时,效率会大幅度下降:吞吐量损失的大小会增加,受影响的uop数量会增加一倍。

  3. 第一个地区有些混乱,7个地方是最差的地方。

对准

上面的DSB和LSD分析是针对循环条目alignment到一个32字节的边界,但是在任何一种情况下,未alignment的情况似乎都没有受到影响:与alignment的情况没有实质性的区别(除了可能的一些小的变化不到10次,我没有进一步调查)。

下面是32N-232N+2的未alignment结果(即32B边界之前和之后的循环前2字节):

错位周期每迭代

理想的N/4线也显示供参考。

Haswell的

接下来看看之前的微架构:Haswell。 这里的数字已经由用户Iwillnotexist Idonotexist慷慨提供。

LSD +传统解码stream水线

首先,testingLSD(对于小的计数)和传统stream水线(对于较大的计数,由于指令密度导致DSB循环“爆发”)的“密码”testing的结果。

我们立刻就看到了每个架构在密集环路中从LSD提供uops的时间差异。 下面我们比较Skylake和Haswell的密码短循环(每个指令1字节)。

哈斯韦尔vs Skylake LSD交付%

如上所述,Skylake循环停止从LSD正确地传送19个uops,正如预期的那样,每32个字节的代码限制的区域为18-uop。 另一方面,Haswell似乎也停止了从LSD可靠地交付16-uop和17-uop的循环。 我对此没有任何解释。 3-uop的情况也有所不同:奇怪的是,两个处理器在3个和4个uop的情况下只能把他们的一些 udp从LSD中提取出来,但是4个uops和3个uop的确切数量是一样的。

我们大多关心实际的performance,对吗? 那么让我们来看一下32字节alignment的密集码情况下的周期/迭代:

Haswell vs Skylake LSD +传统管道

这与Skylake上面显示的数据相同(已经去除了未alignment的系列),并与Haswell一起绘制。 您立即注意到,Haswell的模式是相似的,但不一样。 如上所述,这里有两个地区:

传统解码

从传统解码器传送大于〜16-18uops的循环(以上描述了不确定性)。 Haswell的模式与Skylake有些不同。

对于19-30的范围,他们是相同的,但之后,哈斯韦尔打破了这种模式。 Skylake花费了从传统解码器传送的循环ceil(N/4)循环。 另一方面,Haswell似乎采取类似ceil((N+1)/4) + ceil((N+2)/12) - ceil((N+1)/12) 。 好的,这是凌乱的(更短的forms,任何人?) – 但基本上这意味着,虽然Skylake执行4 * N个循环的循环最佳(即,在4微升/周期),这样的循环(本地)通常是最不理想的计数(至less在本地) – 执行这个循环比Skylake多一个循环。 所以你最好用Haswell的4N-1 uops循环, 除了这样的16-1N(31,47,63等)forms的循环中的25% 需要一个循环。 这听起来像一个闰年的计算 – 但模式可能是最好的视觉上面理解。

我认为这种模式不适合Haswell的调度,所以我们不应该太多的去读。 这似乎是由解释

 0000000000455a80 <short_nop_aligned35.top>: 16B cycle 1 1 455a80: ff c8 dec eax 1 1 455a82: 90 nop 1 1 455a83: 90 nop 1 1 455a84: 90 nop 1 2 455a85: 90 nop 1 2 455a86: 90 nop 1 2 455a87: 90 nop 1 2 455a88: 90 nop 1 3 455a89: 90 nop 1 3 455a8a: 90 nop 1 3 455a8b: 90 nop 1 3 455a8c: 90 nop 1 4 455a8d: 90 nop 1 4 455a8e: 90 nop 1 4 455a8f: 90 nop 2 5 455a90: 90 nop 2 5 455a91: 90 nop 2 5 455a92: 90 nop 2 5 455a93: 90 nop 2 6 455a94: 90 nop 2 6 455a95: 90 nop 2 6 455a96: 90 nop 2 6 455a97: 90 nop 2 7 455a98: 90 nop 2 7 455a99: 90 nop 2 7 455a9a: 90 nop 2 7 455a9b: 90 nop 2 8 455a9c: 90 nop 2 8 455a9d: 90 nop 2 8 455a9e: 90 nop 2 8 455a9f: 90 nop 3 9 455aa0: 90 nop 3 9 455aa1: 90 nop 3 9 455aa2: 90 nop 3 9 455aa3: 75 db jne 455a80 <short_nop_aligned35.top> 

在这里,我已经注意到每个指令出现在16B解码块(1-3)中,以及它将被解码的周期。 规则基本上是直到接下来的4条指令被解码,只要它们落入当前的16B块。 否则,他们必须等到下一个周期。 对于N = 35,我们看到在周期4中有一个解码时隙丢失(在16B块中只剩下3个指令),但是否则环路与16B边界甚至最后一个周期非常一致9)可以解码4条指令。

下面是一个截断的N = 36的例子,除了循环的结尾,它是相同的:

 0000000000455b20 <short_nop_aligned36.top>: 16B cycle 1 1 455a80: ff c8 dec eax 1 1 455b20: ff c8 dec eax 1 1 455b22: 90 nop ... [29 lines omitted] ... 2 8 455b3f: 90 nop 3 9 455b40: 90 nop 3 9 455b41: 90 nop 3 9 455b42: 90 nop 3 9 455b43: 90 nop 3 10 455b44: 75 da jne 455b20 <short_nop_aligned36.top> 

现在有5条指令需要在第3块和最后一块16B块中解码,因此需要一个额外的周期。 基本上35条指令对于这种特定的指令模式恰好与16B位边界alignment,并且在解码时节省一个周期。 这并不意味着N = 35总体上好于N = 36! 不同的指令会有不同的字节数,并且排列不同。 类似的alignment问题也解释了每16个字节需要的附加周期:

 16B cycle ... 2 7 45581b: 90 nop 2 8 45581c: 90 nop 2 8 45581d: 90 nop 2 8 45581e: 90 nop 3 8 45581f: 75 df jne 455800 <short_nop_aligned31.top> 

这里最后的jne已经进入了下一个16B块(如果一个指令跨越了16B的边界,它实际上是在后一个块中),造成额外的周期损失。 这只发生每16个字节。

因此,Haswell传统的解码器结果可以通过一个传统的解码器来完美地解释,该解码器的行为如Agner Fog的微体系结构文档中所述 。 事实上,如果你认为Skylake每个周期可以解码5条指令(最多可以传输5个uops) 9 ,那么它似乎也可以解释Skylake的结果。 假设可以的话,Skylake的这个代码上的渐近传统解码吞吐量仍然是4-uops,因为一个16位的代码块可以解码5-5-5-1,而Haswell则是4-4-4-4,所以你只能得到在边缘的好处:例如,在上面的N = 36的情况下,Skylake可以解码所有剩余的5条指令,而对于Haswell则为4-1,节省了一个周期。

结果似乎是,遗留的解码器行为可以以相当简单的方式来理解,而主要的优化build议是继续按摩代码,使其“巧妙地”落入16Balignment的块(也许这就是NP-硬像垃圾桶包装?)。

DSB(又是LSD)

接下来,让我们来看一下代码从LSD或DSB服务的场景 – 通过使用“long nop”testing来避免打破每32B块限制的18-uop,因此停留在DSB中。

哈斯韦尔vs Skylake:

Haswell vs Skylake LSD和DSB

注意LSD的行为 – 这里的Haswell在57个微博上停止服务LSD,这与57个微软的LSD的发布大小完全一致。 就像我们在Skylake看到的,没有奇怪的“过渡期”。 哈斯韦尔也有奇怪的行为,3和4微微分别只有〜0%和〜40%的微分,来自LSD。

在性能方面,Haswell通常与Skylake有一些偏差,例如65,77和97微微秒左右,在下一个周期中,而Skylake总是能够维持4微微秒/周期,即使是这样的结果在非整数个周期中。 在25日和26日的微弱差距已经消失。 也许Skylake的6-uop传输速率有助于避免Haswell受4-uop传输速率影响的uop-cachealignment问题。

需要帮助

需要帮助来看看这些结果是否适用于较旧和较新的体系结构,并确认这在Skylake上是否属实。 生成这些结果的代码是公开的 。 另外,上面的结果也可以在GitHub中以.ods格式获得。


0特别是,传统解码器的最大吞吐量在Skylake中显然从4增加到5 uops,并且uopcaching的最大吞吐量从4增加到6.这两者都可能影响这里描述的结果。

1英特尔实际上喜欢将遗留解码器称为MITE(微指令翻译引擎),也许是因为这是一种虚假的方式,用遗留的内涵来标记你的架构的任何部分。

2从技术上来说,还有另外一个甚至更慢的微软源码–MS(微码测序引擎),它被用来实现超过4个微指令的任何指令,但是由于我们的循环没有包含微码指令,所以我们忽略这个。

3这是可行的,因为任何alignment的32字节块在它的uop高速caching槽中最多可以使用3个路,每个槽最多可以容纳6个uops。 因此,如果在32B块中使用多于3 * 6 = 18 uops,则代码根本无法存储在uop高速caching中。 在实践中遇到这种情况可能很less见,因为代码需要非常密集(每条指令less于2个字节)才能触发。

4 nop指令解码为一个uop,但在执行之前不会被清除(即,它们不使用执行端口) – 但仍占用前端的空间,因此可以计算出我们的各种限制有兴趣。

5 LSD是循环stream检测器 ,它直接在IDQ中caching高达64(Skylake)uops的小循环。 在早期的体系结构中,它可以容纳28个uops(两个逻辑核心活动)或56个uops(一个逻辑活动核心)。

6在这种模式下,我们不能容易地拟合一个2 uop循环,因为这意味着零nop指令,这意味着decjnz指令将会在uop数量上相应的变化。 只要我的话,所有循环与4个或更less的uops至多执行1循环/迭代。

7为了好玩,我只是在短时间内运行了perf stat ,在这里打开了一个标签并点击了几个Stack Overflow问题。 对于交付的指令,我从DSB得到46%,从传统解码器得到50%,LSD得到4%。 这表明,至less对于像浏览器这样的大型代码,DSB仍然不能捕获绝大多数的代码(幸运的是,传统的解码器并不是太糟糕)。

8这就是说,所有其他的循环计数都可以简单地通过在uops(可能高于实际尺寸是uops)中取一个“有效”积分环路成本并除以4来解释。对于这些非常短的环路,这是行不通的 – 通过将任意整数除以4,每次迭代不能达到1.333个周期。换​​言之,在所有其他区域中,对于某个整数N,成本具有N / 4的forms。

9事实上,我们知道Skylake 可以从传统的解码器每个周期提供5个uops,但我们不知道这5个uops是来自5个不同的指令,还是只有4个或更less。 也就是说,我们预计Skylake可以按2-1-1-1的模式进行解码,但是我不确定它是否可以按照1-1-1-1-1的模式进行1-1-1-1-1 。 上述结果给出了一些证据,certificate它确实可以解码1-1-1-1-1