Performance 英特尔SnB系列CPU上涉及微代码指令的循环的分支对齐

Performance 英特尔SnB系列CPU上涉及微代码指令的循环的分支对齐,performance,assembly,x86,intel,micro-optimization,Performance,Assembly,X86,Intel,Micro Optimization,这与这个问题相关,但不相同:与我之前的问题略有关联: 以下是一个非真实的测试用例。这种素性测试算法是不合理的。我怀疑任何现实世界的算法都不会多次执行如此小的内部循环(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 fals

这与这个问题相关,但不相同:与我之前的问题略有关联:

以下是一个非真实的测试用例。这种素性测试算法是不合理的。我怀疑任何现实世界的算法都不会多次执行如此小的内部循环(
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;
}
我使用
std::chrono::staid\u clock
计时。我不断得到奇怪的性能变化:仅仅添加或删除其他代码。我最终找到了一个对齐问题。命令
.p2align 4,,10
试图对齐到2**4=16字节的边界,但最多只使用10字节的填充,我想这是为了平衡对齐和代码大小

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

从无填充的
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缓存的怪癖吗


通过将C++算法改为Cache <代码> SqRT(num)< /C> > 64位整数,然后使循环纯整数,我就解决了问题——对齐现在根本没什么区别。 您遇到的问题可能不是分支到对齐位置,尽管这仍然有帮助,但您当前的问题更可能是管道机制

当您一个接一个地编写两个说明时,例如:

  mov %eax, %ebx
  add 1, %ebx
为了执行第二条指令,必须完成第一条指令。因此,编译器倾向于混合指令。假设您需要将
%ecx
设置为零,您可以执行以下操作:

  mov %eax, %ebx
  xor %ecx, %ecx
  add 1, %ebx
在这种情况下,
mov
xor
都可以并行执行。这让事情进展得更快。。。处理器之间可以并行处理的指令数量差别很大(Xeon通常更擅长于此)

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

最后,很明显,将
sqrt()
结果转换为整数将大大加快速度,因为您将避免SSE2代码的所有不合理之处。当这两条指令可以使用整数执行时,如果仅用于转换+比较,SSE2代码的速度肯定会减慢


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

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

我没有具体的答案,只是一些我无法测试的不同假设(缺乏硬件)。我想我已经找到了一些结论性的东西,但我的对齐方式偏离了1(因为问题是从0x5F开始计算填充,而不是从对齐的边界)。无论如何,希望这篇文章能对描述可能在这里起作用的因素有所帮助

问题也没有指定分支的编码(短(2B)或近(6B))。这就留下了太多的可能性来研究和理论化到底是哪条指令跨越了32B边界导致了问题


我认为这要么是uop缓存中是否适合循环的问题,要么是与传统解码器的解码速度是否一致的问题。


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

您可能会认为,在除法上出现瓶颈的循环不会在前端出现瓶颈,也不会受到对齐的影响,因为除法很慢,并且循环每个时钟只运行很少的指令。的确如此,但在IvyBridge上,64位DIV被微编码为35-57微操作(UOP),因此可能存在前端问题。 mov %eax, %ebx xor %ecx, %ecx add 1, %ebx
# 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
# 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
.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:
  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>
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>
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
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>
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>
+----------------------------+----------+----------+----------+
|                            | 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%       |
+----------------------------+----------+----------+----------+
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%]