涉及Intel SnB系列CPU上的微编码指令的循环分支对齐

时间:2014-11-13 11:09:59

标签: performance assembly x86 intel micro-optimization

这与此问题相关但不相同:Performance optimisations of x86-64 assembly - Alignment and branch prediction并且与我之前的问题略有关联:Unsigned 64-bit to double conversion: why this algorithm from g++

以下是非真实世界测试用例。这种素性测试算法是不明智的。我怀疑任何现实世界的算法都不会执行如此多的小内循环(num是一个大小约为2 ** 50的素数)。在C ++ 11中:

using nt = unsigned long long;
bool is_prime_float(nt num)
{
   for (nt n=2; n<=sqrt(num); ++n) {
      if ( (num%n)==0 ) { return false; }
   }
   return true;
}

然后g++ -std=c++11 -O3 -S生成以下内容,其中RCX包含n,XMM6包含sqrt(num)。请参阅我之前的帖子,了解剩余代码(在此示例中从未执行过,因为RCX永远不会变得足够大,不能被视为带符号的否定)。

jmp .L20
.p2align 4,,10
.L37:
pxor    %xmm0, %xmm0
cvtsi2sdq   %rcx, %xmm0
ucomisd %xmm0, %xmm6
jb  .L36   // Exit the loop
.L20:
xorl    %edx, %edx
movq    %rbx, %rax
divq    %rcx
testq   %rdx, %rdx
je  .L30   // Failed divisibility test
addq    $1, %rcx
jns .L37
// Further code to deal with case when ucomisd can't be used

我使用std::chrono::steady_clock计时。我一直在进行奇怪的性能变化:从添加或删除其他代码。我最终将其追踪到一个对齐问题。命令.p2align 4,,10尝试对齐2 ** 4 = 16字节边界,但最多只使用10个字节的填充,我想在对齐和代码大小之间取得平衡。

我编写了一个Python脚本,用手动控制的.p2align 4,,10指令替换nop。下面的散点图显示了20次运行中最快的15次,以秒为单位的时间,在x轴上填充的字节数:

Scatter plot

从没有填充的objdump开始,pxor指令将在偏移量0x402f5f处发生。在笔记本电脑上运行,Sandybridge i5-3210m,turboboost 禁用,我发现

  • 对于0字节填充,性能缓慢(0.42秒)
  • 对于1-4字节填充(偏移量0x402f60到0x402f63)稍微好一点(0.41s,在图中可见)。
  • 对于5-20个字节的填充(偏移量0x402f64到0x402f73)获得快速性能(0.37s)
  • 21-32字节填充(偏移量0x402f74到0x402f7f)性能缓慢(0.42秒)
  • 然后循环32字节样本

因此,16字节对齐不能提供最佳性能 - 它使我们处于稍微好一点(或者从散点图中稍微变化)的区域。 32加4到19的对齐可以获得最佳性能。

  

为什么我会看到这种性能差异?为什么这似乎违反了将分支目标与16字节边界对齐的规则(参见例如英特尔优化手册)

我没有看到任何分支预测问题。这可能是一个uop cache quirk ??

通过将C ++算法更改为以64位整数缓存sqrt(num)然后使循环纯粹基于整数,我删除了问题 - 对齐现在没有任何区别。

4 个答案:

答案 0 :(得分:19)

这是我在Skylake上发现的相同循环。所有代码都可以在您的硬件is on github上重现我的测试。

我根据对齐观察了三种不同的性能水平,而OP只看到了2种主要的性能水平。水平非常明显且可重复 2

enter image description here

我们在这里看到三个不同的性能水平(模式从偏移32开始重复),我们称之为区域1,2和3,从左到右(区域2被分成跨越区域3的两个部分) 。最快区域(1)从偏移0到8,中间(2)区域从9-18和28-31,最慢(3)从19-27。 每个区域之间的差异接近或恰好是1个周期/迭代。

基于性能计数器,最快的区域与其他两个区域非常不同:

  • 所有指令均来自传统解码器,而不是来自DSB 1
  • 正好 2解码器&lt; - &gt;微循环开关(idq_ms_switches)用于循环的每次迭代。

另一方面,两个较慢的区域非常相似:

  • 所有指令均来自DSB(uop缓存),而不是传统解码器。
  • 正好有3个解码器&lt; - &gt;每次迭代循环时微码开关。

当偏移从8变为9时,从最快到中间区域的转换完全对应于循环开始适合uop缓冲区的时间,因为对齐问题。你可以用彼得在答案中完全相同的方式来计算它:

偏移8:

  LSD? <_start.L37>:
  ab 1 4000a8:  66 0f ef c0             pxor   xmm0,xmm0
  ab 1 4000ac:  f2 48 0f 2a c1          cvtsi2sd xmm0,rcx
  ab 1 4000b1:  66 0f 2e f0             ucomisd xmm6,xmm0
  ab 1 4000b5:  72 21                   jb     4000d8 <_start.L36>
  ab 2 4000b7:  31 d2                   xor    edx,edx
  ab 2 4000b9:  48 89 d8                mov    rax,rbx
  ab 3 4000bc:  48 f7 f1                div    rcx
  !!!! 4000bf:  48 85 d2                test   rdx,rdx
       4000c2:  74 0d                   je     4000d1 <_start.L30>
       4000c4:  48 83 c1 01             add    rcx,0x1
       4000c8:  79 de                   jns    4000a8 <_start.L37>

在第一列中,我注释了每条指令的uop如何在uop缓存中结束。 &#34; ab 1&#34;表示它们进入与...???a?...???b?等地址相关联的集合(每个集合覆盖32个字节,又称0x20),而1表示方式1(最多3个)。

在这一点上!!!由于test指令无处可去,所以这三种方式都用完了。

让我们看一下偏移9:

00000000004000a9 <_start.L37>:
  ab 1 4000a9:  66 0f ef c0             pxor   xmm0,xmm0
  ab 1 4000ad:  f2 48 0f 2a c1          cvtsi2sd xmm0,rcx
  ab 1 4000b2:  66 0f 2e f0             ucomisd xmm6,xmm0
  ab 1 4000b6:  72 21                   jb     4000d9 <_start.L36>
  ab 2 4000b8:  31 d2                   xor    edx,edx
  ab 2 4000ba:  48 89 d8                mov    rax,rbx
  ab 3 4000bd:  48 f7 f1                div    rcx
  cd 1 4000c0:  48 85 d2                test   rdx,rdx
  cd 1 4000c3:  74 0d                   je     4000d2 <_start.L30>
  cd 1 4000c5:  48 83 c1 01             add    rcx,0x1
  cd 1 4000c9:  79 de                   jns    4000a9 <_start.L37>

现在没有问题! test指令已滑入下一个32B行(cd行),因此所有内容都适合uop缓存。

这解释了为什么在此时MITE和DSB之间发生了变化。但是,它没有解释为什么MITE路径更快。我在循环中使用div尝试了一些更简单的测试,并且您可以使用更简单的循环重现这一点,而不需要任何浮点内容。它对你在循环中放入的随机其他东西很奇怪和敏感。

例如,此循环也比传统解码器执行速度快于DSB:

ALIGN 32
    <add some nops here to swtich between DSB and MITE>
.top:
    add r8, r9
    xor eax, eax
    div rbx
    xor edx, edx
    times 5 add eax, eax
    dec rcx
    jnz .top

在该循环中,添加无意义的add r8, r9指令,该指令并未与循环的其余部分实际交互,为MITE版本加速了(但不是DSB版本。

所以我认为区域1与区域2和区域3之间的差异是由于前者执行传统解码器(奇怪的是,使其更快)。

让我们看一下偏移18到偏移19的过渡(其中region2结束,3开始):

抵消18:

00000000004000b2 <_start.L37>:
  ab 1 4000b2:  66 0f ef c0             pxor   xmm0,xmm0
  ab 1  4000b6: f2 48 0f 2a c1          cvtsi2sd xmm0,rcx
  ab 1  4000bb: 66 0f 2e f0             ucomisd xmm6,xmm0
  ab 1  4000bf: 72 21                   jb     4000e2 <_start.L36>
  cd 1  4000c1: 31 d2                   xor    edx,edx
  cd 1  4000c3: 48 89 d8                mov    rax,rbx
  cd 2  4000c6: 48 f7 f1                div    rcx
  cd 3  4000c9: 48 85 d2                test   rdx,rdx
  cd 3  4000cc: 74 0d                   je     4000db <_start.L30>
  cd 3  4000ce: 48 83 c1 01             add    rcx,0x1
  cd 3  4000d2: 79 de                   jns    4000b2 <_start.L37>

抵消19:

00000000004000b3 <_start.L37>:
  ab 1 4000b3:  66 0f ef c0             pxor   xmm0,xmm0
  ab 1 4000b7:  f2 48 0f 2a c1          cvtsi2sd xmm0,rcx
  ab 1 4000bc:  66 0f 2e f0             ucomisd xmm6,xmm0
  cd 1 4000c0:  72 21                   jb     4000e3 <_start.L36>
  cd 1 4000c2:  31 d2                   xor    edx,edx
  cd 1 4000c4:  48 89 d8                mov    rax,rbx
  cd 2 4000c7:  48 f7 f1                div    rcx
  cd 3 4000ca:  48 85 d2                test   rdx,rdx
  cd 3 4000cd:  74 0d                   je     4000dc <_start.L30>
  cd 3 4000cf:  48 83 c1 01             add    rcx,0x1
  cd 3 4000d3:  79 de                   jns    4000b3 <_start.L37>

我在这里看到的唯一区别是偏移18情况下的前4个指令适合ab缓存行,但偏移19情况下只有3个指令。如果我们假设DSB只能从一个缓存集向IDQ传递uop,这意味着在某个时刻,可以在偏移18场景中比在19场景中更早地发出一个uop并执行一个周期(例如,想象一下IDQ是空的)。根据uop在周围uop流的上下文中到达的确切端口,可能会将循环延迟一个周期。实际上,区域2和区域3之间的差异是~1个周期(在误差范围内)。

所以我认为我们可以说2和3之间的差异可能是由于uop缓存对齐 - 区域2的对齐略好于3,就先前一个周期发布一个额外的uop而言。

我检查过的一些附加说明并没有成为可能造成减速的原因:

  • 尽管DSB模式(区域2和3)具有3个微码开关而不是2个MITE路径(区域1),但似乎直接导致慢一点。特别是,div的简单循环在相同的循环计数中执行,但仍分别为DSB和MITE路径显示3和2个开关。这是正常的,并不直接意味着放缓。

  • 两条路径执行基本相同的uop数,特别是微码定序器生成的uop数相同。因此,在不同地区进行的整体工作并不多。

  • 在不同级别,分支误预测(基本上为零 3 )或任何其他类型的处罚中,缓存未命中(非常低,如预期)确实存在差异或我检查的异常情况。

在各个地区的执行单位使用模式上看到了什么结果。这里看一下每个周期执行的uops分布和一些停顿指标:

+----------------------------+----------+----------+----------+
|                            | Region 1 | Region 2 | Region 3 |
+----------------------------+----------+----------+----------+
| cycles:                    | 7.7e8    | 8.0e8    | 8.3e8    |
| uops_executed_stall_cycles | 18%      | 24%      | 23%      |
| exe_activity_1_ports_util  | 31%      | 22%      | 27%      |
| exe_activity_2_ports_util  | 29%      | 31%      | 28%      |
| exe_activity_3_ports_util  | 12%      | 19%      | 19%      |
| exe_activity_4_ports_util  | 10%      | 4%       | 3%       |
+----------------------------+----------+----------+----------+

我采样了几个不同的偏移值,结果在每个区域内都是一致的,但是在区域之间你得到的结果却截然不同。特别是,在区域1中,您有更少的停顿周期(没有执行uop的周期)。你也可以在非失速周期中有显着的变化,尽管没有明确的&#34;更好的&#34;或者&#34;更糟糕&#34;趋势很明显。例如,区域1具有更多周期(10%对3%或4%),执行了4个uop,但其他区域在很大程度上弥补了它的执行3个uop的更多周期,并且执行了1个uop的周期很少。 / p>

UPC 4 的差异,上面的执行分布意味着完全解释了性能上的差异(这可能是一个重言式,因为我们已经确认它们之间的uop计数相同)。

让我们看看toplev.py对此有什么看法......(结果已省略)。

好吧,toplev认为主要的瓶颈是前端(50 +%)。我不认为你可以相信这一点,因为在长串微编码指令的情况下,它计算FE限制的方式似乎被打破了。 FE限制基于frontend_retired.latency_ge_8,其定义为:

  

在间隔之后获取的退出指令   前端在8个周期内没有发布uops,但没有   被后端摊位打断了。 (支持PEBS)

通常这是有道理的。你正在计算延迟的指令,因为前端没有提供周期。 &#34;不会被后端档位打断&#34;条件确保当前端不能仅仅因为后端无法接受它时(例如,当RS已满,因为后端执行某些低电平时),它不会触发。 throuput说明)。

对于div说明似乎有点 - 即使只有一个div的简单循环也会显示:

FE      Frontend_Bound:                57.59 %           [100.00%]
BAD     Bad_Speculation:                0.01 %below      [100.00%]
BE      Backend_Bound:                  0.11 %below      [100.00%]
RET     Retiring:                      42.28 %below      [100.00%]

也就是说,唯一的瓶颈是前端(&#34;退休&#34;不是瓶颈,它代表了有用的工作)。很明显,这样的循环很容易被前端处理,而是受到后端咀嚼抛出div操作产生的所有uop的能力的限制。 Toplev可能会得到这个错误,因为(1)微码定序器提供的微指令可能不会计入frontend_retired.latency...个计数器,因此每个div操作都会导致该事件计数所有随后的指令(即使CPU在那段时间内很忙 - 没有真正的失速),或者(2)微码音序器可能基本上提供所有的音量&#34;在前面&#34;,猛击~36 uops到IDQ,此时它在div完成之前不再提供,或类似的东西。

不过,我们可以查看较低级别的toplev提示:

区域1和区域2和区域3之间的主要区别是:ms_switches对后两个区域的惩罚增加(因为它们每次迭代都会产生3次,而传统路径则为2次。在内部,{ {1}}估计这些开关的前端有2个周期的惩罚。当然,这些惩罚是否实际上减慢了任何下降取决于指令队列和其他因素的复杂方式。如上所述,一个简单的循环{{ 1}}没有显示DSB和MITE路径之间的任何差异,一个带有附加指令的循环。所以可能是额外的开关气泡被更简单的循环吸收(其中所有uop的后端处理由toplev是主要因素),但是一旦你在循环中添加了一些其他工作,开关就成为至少在div和非div工作之间的过渡期间的一个因素。

所以我猜我的结论是div指令与前端uop流的其余部分以及后端执行的交互方式并未完全理解。我们知道它涉及大量uop,从MITE / DSB(每div似乎4 uop)和微码定序器(每div似乎~32 uops)传递,尽管它随着div op)的不同输入值 - 但我们不知道那些uops是什么(我们可以看到它们的端口分布)。所有这一切都使得行为相当不透明,但我认为这可能取决于MS交换机瓶颈前端,或者uop交付流程的细微差别导致不同的调度决策最终使MITE订单成为主。

1 当然,大多数 uops 根本不是从传统解码器或DSB传送的,而是由微码定序器(ms)传送的。所以我们松散地谈论交付的指令,而不是uops。

2 请注意,此处的x轴是&#34;偏离32B对齐的字节&#34;。也就是说,0表示循环的顶部(标签.L37)与32B边界对齐,5表示循环在32B边界下开始五个字节(使用nop进行填充),依此类推。所以我的填充字节和偏移量是相同的。如果我理解正确的话,OP对偏移使用了不同的含义:他的1字节填充导致0偏移。因此,您将从OPs填充值中减去1以获得我的偏移值。

3 事实上,div的典型测试的分支预测率为 ~99.999997%,在整个运行中仅反映了3个错误预测的分支(可能在第一次通过循环,以及最后一次迭代)。

4 UPC,即每周期 uops - 与类似程序的IPC密切相关的措施,以及在我们详细查看时更精确的措施在uop流动。在这种情况下,我们已经知道对齐的所有变化的uop计数是相同的,因此UPC和IPC将成正比。

答案 1 :(得分:9)

我没有具体的答案,只有一些我无法测试的不同假设(缺乏硬件)。我以为我发现了一些确凿的东西,但我把对齐关闭了(因为问题是从0x5F开始计算填充,而不是从对齐的边界开始)。无论如何,希望无论如何都要发布这个来描述可能在这里发挥作用的因素。

问题也没有指定分支的编码(短(2B)或近(6B))。这留下了太多的可能性来查看和理论确切地说明跨越32B边界的哪条指令导致了这个问题。

我认为它或者是uop缓存中的循环拟合问题,或者它是一个关于它是否与传统解码器快速解码的对齐问题。

显然asm循环可以得到很多改进(例如,通过提升浮点数,更不用说完全使用不同的算法),但这不是问题。我们只想知道为什么对齐对于这个确切的循环很重要。

您可能期望分区上的瓶颈不会在前端遇到瓶颈或受到对齐的影响,因为除法很慢且每个时钟循环运行的指令非常少。这是真的,但 64位DIV在IvyBridge上被微编码为35-57微操作(uops),因此事实证明可能存在前端问题。

对齐关键的两个主要方式是:

我怀疑这是一个纯粹的前端问题,而不是分支预测,因为代码会花费所有时间在这个循环中,并且不会运行其他分支,这些分支可能与这里的分支相同。

您的英特尔IvyBridge CPU是SandyBridge的缩小版。它有一些变化(如mov-elimination和ERMSB),但前端在SnB / IvB / Haswell之间类似。 Agner Fog's microarch pdf有足够的细节来分析CPU运行此代码时应该发生什么。另请参阅David Kanter's SandyBridge writeup for a block diagram of the fetch/decode stages,但他从uop缓存,微代码和decode-uop队列中分离了fetch / decode。最后,有一个完整核心的完整框图。他的Haswell文章有一个框图,包括整个前端,直到为问题阶段提供信息的解码uop队列。 (IwellBridge和Haswell一样,在不使用超线程时有56个uop队列/环回缓冲区。即使HT被禁用,Sandybridge也会将它们静态分区为2x28 uop队列。)

David Kanter's SnB writeup

David Kanter's also-excellent Haswell write-up复制的图像,其中包含解码器和uop-cache在一个图表中。

让我们看一下,一旦事情稳定下来,uop缓存可能会缓存这个循环。 (即假设循环中间带有jmp的循环条目对循环在uop缓存中的位置没有任何严重的长期影响)。

根据英特尔优化手册( 2.3.2.2解码的ICache ):

  • 所有微操作方式(uop缓存行)代表在代码中静态连续的指令并具有 它们的EIP位于同一对齐的32字节区域内。 (我认为这意味着一个超出边界的指令进入包含其开始的块的uop缓存,而不是结束。生成指令必须在某处,并且运行指令的分支目标地址是该指令的开始。 insn,所以把它放在一行中是最有用的。)
  • 不能跨方式分割多微操作指令。
  • 打开MSROM的指令会消耗整个路径。 (即,任何超过4 uops的指令(对于reg,reg形式)都是微编码的。例如,DPPD不是微编码的(4 uop),但是DPPS是(6 uop)。具有内存操作数的DPPD可以& #39;微型保险丝总共5个uop,但仍然不需要打开微码定序器(未测试)。
  • 每条路最多允许两个分支。
  • 将一对宏融合指令保存为一个微操作。

David Kanter的SnB写作还有更多great details about the uop cache

让我们看看实际代码将如何进入uop缓存

# let's consider the case where this is 32B-aligned, so it runs in 0.41s
# i.e. this is at 0x402f60, instead of 0 like this objdump -Mintel -d output on a  .o
# branch displacements are all 00, and I forgot to put in dummy labels, so they're using the rel32 encoding not rel8.

0000000000000000 <.text>:
   0:   66 0f ef c0             pxor   xmm0,xmm0    # 1 uop
   4:   f2 48 0f 2a c1          cvtsi2sd xmm0,rcx   # 2 uops
   9:   66 0f 2e f0             ucomisd xmm6,xmm0   # 2 uops
   d:   0f 82 00 00 00 00       jb     0x13         # 1 uop  (end of one uop cache line of 6 uops)

  13:   31 d2                   xor    edx,edx      # 1 uop
  15:   48 89 d8                mov    rax,rbx      # 1 uop  (end of a uop cache line: next insn doesn't fit)

  18:   48 f7 f1                div    rcx          # microcoded: fills a whole uop cache line.  (And generates 35-57 uops)

  1b:   48 85 d2                test   rdx,rdx      ### PROBLEM!!  only 3 uop cache lines can map to the same 32-byte block of x86 instructions.
  # So the whole block has to be re-decoded by the legacy decoders every time, because it doesn't fit in the uop-cache
  1e:   0f 84 00 00 00 00       je     0x24         ## spans a 32B boundary, so I think it goes with TEST in the line that includes the first byte.  Should actually macro-fuse.
  24:   48 83 c1 01             add    rcx,0x1      # 1 uop 
  28:   79 d6                   jns    0x0          # 1 uop

因此,对于循环开始的32B对齐,它必须从传统解码器运行,这可能比从uop缓存运行要慢。从uop缓存切换到传统解码器甚至可能会有一些开销。

@Iwill的测试(请参阅问题评论)显示 任何微编码指令都会阻止循环从环回缓冲区运行。查看有关该问题的评论。 (LSD =循环流检测器=循环缓冲区;物理上与IDQ(指令解码队列)结构相同.DSB =解码流缓冲区= uop缓存.MITE =传统解码器。)

即使循环足够小以便从LSD运行(最小为28 uops,或者在IvB和Haswell上没有超线程的56),强制uop缓存也会损害性能。

英特尔优化手册(第2.3.2.4节)表示LSD要求包括

  
      
  • 所有微操作也都驻留在Decoded ICache中。
  •   

因此,这解释了为什么微码没有资格:在这种情况下,uop-cache只保存指向微码的指针,而不是uops本身。另请注意,这意味着出于任何其他原因(例如,大量单字节NOP指令)破坏uop缓存意味着循环无法从LSD运行。

根据OP的测试,最快填充

# branch displacements are still 32-bit, except the loop branch.
# This may not be accurate, since the question didn't give raw instruction dumps.
# the version with short jumps looks even more unlikely

0000000000000000 <loop_start-0x64>:
    ...
  5c:   00 00                   add    BYTE PTR [rax],al
  5e:   90                      nop
  5f:   90                      nop

  60:   90                      nop         # 4NOPs of padding is just enough to bust the uop cache before (instead of after) div, if they have to go in the uop cache.
          # But that makes little sense, because looking backward should be impossible (insn start ambiguity), and we jump into the loop so the NOPs don't even run once.
  61:   90                      nop
  62:   90                      nop
  63:   90                      nop

0000000000000064 <loop_start>:                   #uops #decode in cycle A..E
  64:   66 0f ef c0             pxor   xmm0,xmm0   #1   A
  68:   f2 48 0f 2a c1          cvtsi2sd xmm0,rcx  #2   B
  6d:   66 0f 2e f0             ucomisd xmm6,xmm0  #2   C (crosses 16B boundary)
  71:   0f 82 db 00 00 00       jb     152         #1   C

  77:   31 d2                   xor    edx,edx     #1   C
  79:   48 89 d8                mov    rax,rbx     #1   C

  7c:   48 f7 f1                div    rcx       #line  D

  # 64B boundary after the REX in next insn    
  7f:   48 85 d2                test   rdx,rdx     #1   E
  82:   74 06                   je     8a <loop_start+0x26>#1 E
  84:   48 83 c1 01             add    rcx,0x1     #1   E
  88:   79 da                   jns    64 <loop_start>#1 E

test rdx,rdx的REX前缀与DIV位于同一个块中,因此这应该会破坏uop缓存。再填充一个字节会将它放入下一个32B块,这将是完美的意义。也许OP的结果是错误的,或者前缀可能不算数,并且它是重要的操作码字节的位置。也许这很重要,或者可能是宏观融合测试+分支被拉到下一个区块?

宏融合确实发生在64B L1I-cache行边界上,因为它不会落在指令之间的边界上。

  

如果第一条指令在高速缓存行的字节63上结束,并且第二条指令是从下一个高速缓存行的字节0开始的条件分支,则不会发生宏融合。 - 英特尔的优化手册,2.3.2.1

或者对于一个跳跃或另一个跳跃的短编码,情况有所不同?

或者破坏uop缓存可能与它无关,只要它快速解码,这种对齐就会发生,这很好。这个填充量几乎没有将UCOMISD的结尾放到一个新的16B块中,所以可能通过让它与下一个对齐的16B块中的其他指令一起解码来提高效率。但是,我不确定16B预解码(指令长度查找)或32B解码块是否必须对齐。

我还想知道CPU是否会频繁地从uop缓存切换到传统解码。这可能比从传统解码一直运行更糟糕。

根据Agner Fog的微型指南,从解码器切换到uop缓存或反之亦然需要一个周期。英特尔说:

  

由于这些限制,当微操作无法存储在解码的ICache中时,它们将从传统解码管道传送。一旦从传统管道传递微操作,就可以获取微操作   来自Decoded ICache的ops只能在下一个分支微操作之后恢复。频繁的开关可能会受到惩罚。

我组装的来源+反汇编:

.skip 0x5e
nop
# this is 0x5F
#nop  # OP needed 1B of padding to reach a 32B boundary

.skip 5, 0x90

.globl loop_start
loop_start:
.L37:
  pxor    %xmm0, %xmm0
  cvtsi2sdq   %rcx, %xmm0
  ucomisd %xmm0, %xmm6
  jb  .Loop_exit   // Exit the loop
.L20:
  xorl    %edx, %edx
  movq    %rbx, %rax
  divq    %rcx
  testq   %rdx, %rdx
  je  .Lnot_prime   // Failed divisibility test
  addq    $1, %rcx
  jns .L37

.skip 200  # comment this to make the jumps rel8 instead of rel32
.Lnot_prime:
.Loop_exit:

答案 2 :(得分:-1)

从我在算法中看到的内容来看,你无法做很多事情来改进它。

你遇到的问题可能不是分支到一个对齐位置,尽管这仍然有帮助,你当前的问题更可能是管道机制。

当你一个接一个地写下两个指令时,例如:

  mov %eax, %ebx
  add 1, %ebx

为了执行第二条指令,必须完成第一条指令。因此,编译器倾向于混合指令。假设您需要将%ecx设置为零,您可以这样做:

  mov %eax, %ebx
  xor %ecx, %ecx
  add 1, %ebx

在这种情况下,movxor都可以并行执行。这使事情变得更快......并行处理的指令数量在处理器之间变化很大(Xeons通常更好)。

分支添加另一个参数,其中最佳处理器可以同时开始执行分支的两侧(true和false ...)。但实际上大多数处理器都会猜测并希望它们是正确的。

最后,显而易见的是,将sqrt()结果转换为整数将使很多更快,因为您将避免SSE2代码的所有无意义,如果SSE2代码明显更慢仅用于转换+比较,这两个指令可以用整数完成。

现在......你可能仍然想知道为什么对齐与整数无关。事实是,如果您的代码适合L1指令缓存,那么对齐并不重要。如果丢失了L1缓存,那么它必须重新加载代码,并且对齐变得非常重要,因为在每个循环上它可能会加载无用的代码(可能是15个字节的无用代码......)和内存访问仍然很慢。

答案 3 :(得分:-2)

性能差异可以通过指令编码机制“看到”指令的不同方式来解释。 CPU以块的形式读取指令(我相信在core2 16字节上),并尝试给出不同的超标量单位微指令。如果指令在边界上或者不太可能被命令,则一个核心中的单元可能很容易饿死。