SchönauerTriadBenchmark - L1D Cache不可见

时间:2017-05-28 14:51:02

标签: c caching gcc benchmarking hpc

我们是两名HPC学生参与了着名的SchönauerTriadBenchmark,其中的C代码及其简短说明如下:

var layoutS = 
{  
   title:'Plot',
   xaxis1:{  
      zeroline:false,
      zerolinewidth:1,
      showline:true,
      showticklabels:true,
      ticks:'outside',

   },
   yaxis1:{  
      zeroline:true,
      zerolinewidth:1,
      showline:true,
      showticklabels:true,
      ticks:'outside',

   },
   width:graphWidth,
   height:graphHeight,
   annotations:[  
      {  
         xref:'paper',
         yref:'paper',
         x:0,
         xanchor:'center',
         y:1.1,
         yanchor:'bottom ',
         text:'x[n]',
         showarrow:false
      },
      {  
         xref:'paper',
         yref:'paper',
         x:1.1,
         xanchor:'left',
         y:0,
         yanchor:'top',
         text:'n',
         showarrow:false
      }
   ]
}

代码只是循环遍历N,对于每个 N ,它执行 NR 浮点运算,其中 NR 是一个代表N的常量每次最外迭代时要进行的常数操作次数,以便即使对于太短的N值也能进行准确的时间测量。要分析的内核显然是 simulation 子例程。

我们得到了一些奇怪的结果:

我们开始在 E4 E9220服务器2U 上对内核进行基准测试,该服务器由8个节点组成,每个节点都配有双插槽 Intel Xeon E5-2697 V2(Ivy Bridge)@ 2,7 GHz,12核。该代码已使用 gcc(GCC)4.8.2 进行编译,并已在 Linux CentOS版本6 上运行。下面列出了单个图像中的结果图:

N versus MFlops plots: -Ofast (above) and -Ofast along -march=native (below)

直接看到L2和L3下坡非常明显,并且通过做一些简单的计算并考虑到多道程序设计问题以及L2-L3统一且L3在所有12个中共享的事实在数值上是可以的。核心。在第一个图中,L1是不可见的,而在第二个图中,它是可见的,它以N值开始,因此根据每个核心的L1D大小,得到的L1D饱和度值正好是32 KB。第一个问题是:为什么我们不在没有 -march = native 架构专业化标志的​​情况下看到L1下坡?

经过一些棘手的(明显错误的)自我解释后,我们决定在 Lenovo Z500 上进行基准测试,配备单个插座 Intel Core i7-3632QM(Ivy Bridge)@ 2.2 GHz 。这次我们使用了gcc (Ubuntu 6.3.0-12ubuntu2)6.3.0 20170406 (来自 gcc --version ),结果图如下:< / p>

N versus MFlops plots: -Ofast (above) and -Ofast along -march=native (below)

第二个问题有些自发:为什么我们看到L1D下坡而没有 -march = native - 这次?

1 个答案:

答案 0 :(得分:0)

内部“TRIAD”循环的汇编片段(A[i] = B[i] + C[i]*D[i]:每i次迭代2次double_precision触发器,3次读取double,1次写入double。

来自perf annotate的确切百分比并不是非常有用,因为您将具有不同性能的所有区域分析为单次运行。并且long perf报告根本没有用,通常只需要#之后的5-10个第一行。您可以尝试将测试限制在4 * N * sizeof(double)<4的感兴趣区域。 sizeof(L1d_cache)并重新收集perf注释并获得perf stat ./programperf stat -d ./program的结果(并且还了解特定于英特尔的性能包装器ocperf.py - https://github.com/andikleen/pmu-tools以及其他工具)

gcc-6.3.0 -Ofast - 使用128位(2个双精度)XMM registersSSE2 movupd/movups(SSE2是x86_64 cpu的默认FPU),每次i次迭代2次汇编程序循环(movupd从内存加载2个双打)

         :                              A[i] = B[i] + C[i]*D[i];
    0.03 :        d70:       movupd (%r11,%rax,1),%xmm1    # load C[i:i+1] into xmm1
   14.87 :        d76:       add    $0x1,%ecx              # advance 'i/2' loop counter by 1
    0.10 :        d79:       movupd (%r10,%rax,1),%xmm0    # load D[i:i+1] into xmm0
   14.59 :        d7f:       mulpd  %xmm1,%xmm0            # multiply them into xmm0
    2.78 :        d83:       addpd  (%r14,%rax,1),%xmm0    # load B[i:i+1] and add to xmm0
   17.69 :        d89:       movups %xmm0,(%rsi,%rax,1)    # store into A[i:i+1]
    2.71 :        d8d:       add    $0x10,%rax             # advance array pointer by 2 doubles (0x10=16=2*8)
    1.68 :        d91:       cmp    %edi,%ecx              # check for end of loop (edi is N/2)
    0.00 :        d93:       jb     d70 <main+0x4c0>       # if not, jump to 0xd70

来自gcc-6.3.0 -Ofast -march=native:vmovupd不仅仅是向量(SSE2 某些 pd也是向量),它们是AVX instructions,可以使用2倍宽的寄存器YMM(256位,每个寄存器4个双打)。循环时间较长,但每次循环迭代处理4 i次迭代

    0.02 :        db6:       vmovupd (%r10,%rdx,1),%xmm0   # load C[i:i+1] into xmm0 (low part of ymm0)
    8.42 :        dbc:       vinsertf128 $0x1,0x10(%r10,%rdx,1),%ymm0,%ymm1  # load C[i+2:i+3] into high part of ymm1 and copy xmm0 into lower part; ymm1 is C[i:i+3]
    7.37 :        dc4:       add    $0x1,%esi              # loop counter ++
    0.06 :        dc7:       vmovupd (%r9,%rdx,1),%xmm0    # load D[i:i+1] -> xmm0
   15.05 :        dcd:       vinsertf128 $0x1,0x10(%r9,%rdx,1),%ymm0,%ymm0  # load D[i+2:i+3] and get D[i:i+3] in ymm0
    0.85 :        dd5:       vmulpd %ymm0,%ymm1,%ymm0      # mul C[i:i+3] and D[i:i+3] into ymm0
    1.65 :        dd9:       vaddpd (%r11,%rdx,1),%ymm0,%ymm0  # soad 4 doubles of B[i:i+3] and add to ymm0
   21.18 :        ddf:       vmovups %xmm0,(%r8,%rdx,1)    # store low 2 doubles to A[i:i+1]
    1.24 :        de5:       vextractf128 $0x1,%ymm0,0x10(%r8,%rdx,1)  # store high 2 doubles to A[i+2:i+3]
    2.04 :        ded:       add    $0x20,%rdx             # advance array pointer by 4 doubles
    0.02 :        df1:       cmp    -0x460(%rbp),%esi      # loop cmp
    0.00 :        df7:       jb     db6 <main+0x506>       # loop jump to 0xdb6

启用AVX的代码(使用-march=native)更好,因为它使用更好的展开,但它使用2个双倍的窄负载。通过更多真实测试,阵列将更好地对齐,编译器可以选择最宽256-bit vmovupd到ymm,而无需插入/提取指令。

你现在的代码可能很慢 无法完全加载(饱和接口到L1数据缓存在大多数情况下使用短数组。另一种可能是阵列之间的对齐不良。

https://i.stack.imgur.com/2ovxm.png - 6“GFLOPS”的下图中,您的高带宽短暂出现,这很奇怪。进行计算以将其转换为GByte / s并找到Ivy Bridge的L1d带宽和负载发布率的限制......类似于https://software.intel.com/en-us/forums/software-tuning-performance-optimization-platform-monitoring/topic/532346 Haswell核心每个周期只能发出两个负载,所以它们必须是256位AVX负载,才有可能达到64字节/周期的速率。“(TRIAD的专家和STREAM的作者,John D. McCalpin,博士”Bandwidth博士“ ,搜索他的帖子)和http://www.overclock.net/t/1541624/how-much-bandwidth-is-in-cpu-cache-and-how-is-it-calculated L1带宽取决于每个滴答的指令和指令的步幅(AVX = 256位,SSE = 128位等).IIRC,Sandy Bridge每个tick有1条指令