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

Mat*_*aws 21 performance x86 assembly intel micro-optimization

这与此问题有关,但不一样:x86-64汇编的性能优化 - 对齐和分支预测与我之前的问题略有关系:无符号64位到双倍转换:为什么这个算法来自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;
}
Run Code Online (Sandbox Code Playgroud)

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

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

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

散点图

objdump没有填充,将发生在偏移0x402f5f的PXOR指令.在笔记本电脑上运行,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缓存怪癖?

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

Bee*_*ope 19

这是我在Skylake上发现的相同循环.所有在硬件重现我测试的代码都在github上.

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

在此输入图像描述

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

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

  • 所有指令都是从传统解码器传送的,而不是从DSB 1传送的.
  • 正好 2解码器< - >微开关(idq_ms_switches),用于循环的每一次迭代.

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

  • 所有指令都是从DSB(uop缓存)传送的,而不是来自传统解码器.
  • 每次迭代循环正好有3个解码器< - >微码开关.

当偏移从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>
Run Code Online (Sandbox Code Playgroud)

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

在这一点!!! 这会从uop缓存中消失,因为test指令无处可去,所有3种方式都用完了.

让我们看看另一方面的偏移量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>
Run Code Online (Sandbox Code Playgroud)

现在没有问题!该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
Run Code Online (Sandbox Code Playgroud)

在该循环中,添加无意义的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>
Run Code Online (Sandbox Code Playgroud)

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

我在这里看到的唯一区别是偏移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个开关.这是正常的,并不直接意味着放缓.

  • 两个路径执行基本上相同数量的微指令,并且特别地,具有由微代码定序器生成的相同数量的微指令.因此,并不是说在不同地区有更多的整体工作要做.

  • 有没有真正的高速缓存未命中各级的差异(非常低,如预期),分支预测错误(基本上为零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%       |
+----------------------------+----------+----------+----------+
Run Code Online (Sandbox Code Playgroud)

我对几个不同的偏移值进行了采样,结果在每个区域内都是一致的,但在区域之间你得到了截然不同的结果.特别是,在区域1中,您有更少的停顿周期(没有执行uop的周期).尽管没有明显的"更好"或"更差"趋势,但非失速周期也存在显着差异.例如,区域1具有更多周期(10%对3%或4%),执行4个uop,但其他区域在很大程度上弥补了它的执行3个uop的更多周期,并且执行1个uop的周期很少.

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

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

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

退回指令,这些指令是在前端在8个周期内没有发出uops而没有被后端停顿中断的间隔之后获取的.(支持PEBS)

通常这是有道理的.您正在计算由于前端未提供周期而延迟的指令."没有被后端停顿中断"条件确保当前端不传递微操作时不会触发这只是因为后端无法接受它们(例如,当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%]
Run Code Online (Sandbox Code Playgroud)

也就是说,唯一的瓶颈是前端("退休"不是瓶颈,它代表了有用的工作).很明显,这样的循环很容易被前端处理,而是受到后端咀嚼div操作产生的所有uop的能力的限制.Toplev可能会得到这个错误,因为(1)微码定序器发送的微指令可能不会计入frontend_retired.latency...计数器中,因此每个div操作都会导致该事件计算所有后续指令(即使CPU处于忙碌状态)那个时期 - 没有真正的失速),或者(2)微码音序器可能基本上"提前"发出它所有的声音,向IDQ猛击~36微秒,此时它不再发送直到div完成, 或类似的东西.

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

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

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


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

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

3事实上,对于一个典型的试验中的分支预测速率prime=1000000000000037〜99.999997% ,反射仅3误预测,在整个运行分支(有可能在第一次通过循环,最后迭代).

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

  • 我最近测试了很多这样的东西,并且使用`perf`禁用HT对我的测量的稳定性有很大的和可重现的影响.这是有道理的,因为操作系统可能偶尔会在另一个逻辑核心上安排其他线程,这可能会减少线程可用的资源.这是最大的帮助. (2认同)
  • 禁用turbo(我使用[此脚本](http://askubuntu.com/a/619881/523336))和各种电源管理功能似乎也有所帮助.它对挂钟和CPU时间(这是有道理的)产生了很大的不同,但也有一些差异(我认为)与无卤的循环计数有关.正如你所指出的那样,这似乎很奇怪,因为周期应该或多或少地对这些东西不变.仍然,转换可能导致计数改变(例如,如果管道被刷新),并且当时速率比改变时,当然任何访问存储器或(在某些情况下L3-L4)的事件都会受到影响. (2认同)

Pet*_*des 9

我没有具体的答案,只有一些我无法测试的不同假设(缺乏硬件).我想我发现了一些定论,但我有对准偏离一个(因为这个问题从0x5F的计数填充,而不是从一个对准边界).无论如何,希望无论如何发布它来描述可能在这里发挥作用的因素是有用的.

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


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


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

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

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

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

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

David Kanter的SnB写作

图像复制自大卫坎特的优秀Haswell写作,其中包括解码器和uop-cache在一个图表中.

让我们来看看uop缓存如何在事情稳定后缓存这个循环.(即假设循环中间带有jmp的循环条目对循环在uop缓存中的位置没有任何严重的长期影响).

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

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

David Kanter的SnB写作有一些关于uop缓存的更多细节.


让我们看看实际代码将如何进入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
Run Code Online (Sandbox Code Playgroud)

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

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

即使循环小到足以从LSD运行(至少28 uop,或者在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
Run Code Online (Sandbox Code Playgroud)

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

宏融合确实发生在64B L1I高速缓存线边界上,因为它不落在指令之间的边界上.

如果第一指令上的高速缓存行的字节63结束,并且第二指令是启动在下一高速缓冲存储器线的字节0的条件分支宏融合不会发生. - 英特尔的优化手册,2.3.2.1

或者对于一个跳跃或另一个跳跃的短编码,事情是不同的?

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


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

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

由于这些限制,当微操作无法存储在解码的ICache中时,它们将从传统解码管道传送.一旦微操作是从传统的管道提供,获取从解码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:
Run Code Online (Sandbox Code Playgroud)

  • @PeterCordes - 我在下面添加了一些我在Skylake上发现的细节.特别是,uop缓存肯定会影响它:某些对齐将1个uop推入下一个缓存行(注意,不同于下一个"方式"),这可能导致uop稍后出现在IDQ中并可能最终减慢循环一个循环.我还发现了如上所述的"破坏uop"缓存效果,但其效果与您的预期相反:当uop缓存被"破坏"并且代码从MITE发出时,我们获得了最佳性能! (2认同)

Ale*_*lke -1

从我在你的算法中看到的情况来看,你肯定没有太多可以改进的地方。

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

当您依次编写两条指令时,例如:

  mov %eax, %ebx
  add 1, %ebx
Run Code Online (Sandbox Code Playgroud)

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

  mov %eax, %ebx
  xor %ecx, %ecx
  add 1, %ebx
Run Code Online (Sandbox Code Playgroud)

在这种情况下,mov和 都xor可以并行执行。这使得事情变得更快...处理器之间可以并行处理的指令数量差异很大(Xeon 通常在这方面更好)。

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

最后,很明显,将结果转换sqrt()为整数将使事情变得更快,因为您将避免 SSE2 代码的所有无意义,如果仅用于转换 + 比较(当这两条指令可以用整数。

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

  • 另外:“最好的处理器可能会同时开始执行分支的两侧(真和假......)。” 据我所知,没有微架构可以推测分支的两侧。是的,这在理论上是可行的设计,但没有人这样做。我也不确定答案的前半部分(关于指令级并行性)有什么帮助。(不,Xeon 没有更宽的乱序核心,或者在不受缓存未命中限制的单个线程中拥有更多 ILP。Xeon 拥有与 i7 相同核心的“更多”核心,但那是线程-级别并行,而不是指令级别。) (2认同)

归档时间:

查看次数:

2156 次

最近记录:

8 年,1 月 前