矢量化代码时缓存未命中数增加

fc6*_*c67 17 c++ profiling caching vectorization papi

我在SSE 4.2和AVX 2的2个向量之间矢量化了点积,如下所示.该代码使用GCC 4.8.4和-O2优化标志进行编译.正如预期的那样,两者的性能都有所提高(和AVX 2比SSE 4.2快),但是当我用PAPI分析代码时,我发现未命中的总数(主要是L1和L2)增加了很多:

没有矢量化:

PAPI_L1_TCM: 784,112,091
PAPI_L2_TCM: 195,315,365
PAPI_L3_TCM: 79,362
Run Code Online (Sandbox Code Playgroud)

使用SSE 4.2:

PAPI_L1_TCM: 1,024,234,171
PAPI_L2_TCM: 311,541,918
PAPI_L3_TCM: 68,842
Run Code Online (Sandbox Code Playgroud)

使用AVX 2:

PAPI_L1_TCM: 2,719,959,741
PAPI_L2_TCM: 1,459,375,105
PAPI_L3_TCM: 108,140
Run Code Online (Sandbox Code Playgroud)

我的代码可能有问题或者这种行为是否正常?

AVX 2代码:

double vec_dotProduct(const vec& vecs, const unsigned int& start_a, const unsigned int& start_b, const int& n) {
    double dot = 0;
    register int i = 0;
    const int loopBound = n-3;

    __m256d vsum, vecPi, vecCi, vecQCi;

    vsum = _mm256_set1_pd(0);

    double * const pA = vecs.x+start_a ;
    double * const pB = vecs.x+start_b ;

    for( ; i<loopBound ;i+=4){
        vecPi  = _mm256_loadu_pd(&(pA)[i]);
        vecCi  = _mm256_loadu_pd(&(pB)[i]);
        vecQCi = _mm256_mul_pd(vecPi,vecCi);
        vsum   = _mm256_add_pd(vsum,vecQCi);
    }

    vsum = _mm256_hadd_pd(vsum, vsum);

    dot = ((double*)&vsum)[0] + ((double*)&vsum)[2];

    for( ; i<n; i++)
        dot += pA[i] * pB[i];

    return dot;
}
Run Code Online (Sandbox Code Playgroud)

SSE 4.2代码:

double vec_dotProduct(const vec& vecs, const unsigned int& start_a, const unsigned int& start_b, const int& n) {
    double dot = 0;
    register int i = 0;

    const int loopBound = n-1;

    __m128d vsum, vecPi, vecCi, vecQCi;

    vsum = _mm_set1_pd(0);

    double * const pA = vecs.x+start_a ;
    double * const pB = vecs.x+start_b ;

    for( ; i<loopBound ;i+=2){
        vecPi  = _mm_load_pd(&(pA)[i]);
        vecCi  = _mm_load_pd(&(pB)[i]);
        vecQCi = _mm_mul_pd(vecPi,vecCi);
        vsum   = _mm_add_pd(vsum,vecQCi);
    }

    vsum = _mm_hadd_pd(vsum, vsum);

    _mm_storeh_pd(&dot, vsum);

    for( ; i<n; i++)
        dot += pA[i] * pB[i];

    return dot;
}
Run Code Online (Sandbox Code Playgroud)

非矢量化代码:

double dotProduct(const vec& vecs, const unsigned int& start_a, const unsigned int& start_b, const int& n) {
    double dot = 0;
    register int i = 0;

    for (i = 0; i < n; ++i)
    {
        dot += vecs.x[start_a+i] * vecs.x[start_b+i];
    }
    return dot;
}
Run Code Online (Sandbox Code Playgroud)

编辑:非矢量化代码的汇编:

   0x000000000040f9e0 <+0>:     mov    (%rcx),%r8d
   0x000000000040f9e3 <+3>:     test   %r8d,%r8d
   0x000000000040f9e6 <+6>:     jle    0x40fa1d <dotProduct(vec const&, unsigned int const&, unsigned int const&, int const&)+61>
   0x000000000040f9e8 <+8>:     mov    (%rsi),%eax
   0x000000000040f9ea <+10>:    mov    (%rdi),%rcx
   0x000000000040f9ed <+13>:    mov    (%rdx),%edi
   0x000000000040f9ef <+15>:    vxorpd %xmm0,%xmm0,%xmm0
   0x000000000040f9f3 <+19>:    add    %eax,%r8d
   0x000000000040f9f6 <+22>:    sub    %eax,%edi
   0x000000000040f9f8 <+24>:    nopl   0x0(%rax,%rax,1)
   0x000000000040fa00 <+32>:    mov    %eax,%esi
   0x000000000040fa02 <+34>:    lea    (%rdi,%rax,1),%edx
   0x000000000040fa05 <+37>:    add    $0x1,%eax
   0x000000000040fa08 <+40>:    vmovsd (%rcx,%rsi,8),%xmm1
   0x000000000040fa0d <+45>:    cmp    %r8d,%eax
   0x000000000040fa10 <+48>:    vmulsd (%rcx,%rdx,8),%xmm1,%xmm1
   0x000000000040fa15 <+53>:    vaddsd %xmm1,%xmm0,%xmm0
   0x000000000040fa19 <+57>:    jne    0x40fa00 <dotProduct(vec const&, unsigned int const&, unsigned int const&, int const&)+32>
   0x000000000040fa1b <+59>:    repz retq 
   0x000000000040fa1d <+61>:    vxorpd %xmm0,%xmm0,%xmm0
   0x000000000040fa21 <+65>:    retq   
Run Code Online (Sandbox Code Playgroud)

编辑2:下面你可以找到矢量化和非矢量化代码之间的L1缓存未命中对比较大的N(x标签上的N和y标签上的L1缓存未命中).基本上,对于更大的N,矢量化版本中的失误仍然多于非矢量化版本.

在此输入图像描述

小智 1

Rostislav 是正确的,编译器是自动向量化的,并且来自 GCC 关于 -O2 的文档:

“-O2 进一步优化。GCC 执行几乎所有支持的优化,不涉及空间速度权衡。” (从这里: https: //gcc.gnu.org/onlinedocs/gcc/Optimize-Options.html

带有 -O2 标志的 GCC 正在尝试生成最有效的代码,而不偏向代码大小或速度。

因此,就 CPU 周期而言,-O2 自动矢量化代码运行所需的功耗最低,但不会是最快或最小的代码。对于在移动设备和多用户系统上运行的代码来说这是最好的情况,并且这些往往是 C++ 的首选使用。如果您想要绝对最大速度,无论使用多少瓦特,请尝试 -O3 或 -Ofast(如果您的 GCC 版本支持),或者采用手动优化的更快解决方案。

造成这种情况的原因可能是两个因素的结合。

首先,更快的代码会在相同的时间内生成更多对内存/缓存的请求,这会给预取预测算法带来压力。L1 缓存不是很大,通常为 1MB - 3MB,并且在该 CPU 核心上的所有正在运行的进程之间共享,因此 CPU 核心无法预取,直到先前预取的块不再使用。如果代码运行得更快,则块之间预取的时间就会更少,并且在有效流水线的代码中,在 CPU 核心完全停止之前将执行更多的缓存未命中,直到挂起的提取完成。

其次,现代操作系统通常通过动态调整线程亲和性在多个核心之间划分单线程进程,以便在多个核心之间利用额外的缓存,即使它不能并行运行任何代码 - 例如填充核心 0缓存数据,然后在填充核心 1 的缓存时运行它,然后在重新填充核心 0 的缓存时在核心 1 上运行,循环直至完成。这种伪并行提高了单线程进程的整体速度,并且应该大大减少缓存未命中,但只能在非常特定的情况下完成......良好的编译器将尽可能生成代码的特定情况。