矢量化代码

时间:2015-12-03 14:50:18

标签: c++ caching profiling 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

使用SSE 4.2:

PAPI_L1_TCM: 1,024,234,171
PAPI_L2_TCM: 311,541,918
PAPI_L3_TCM: 68,842

使用AVX 2:

PAPI_L1_TCM: 2,719,959,741
PAPI_L2_TCM: 1,459,375,105
PAPI_L3_TCM: 108,140

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

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;
}

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;
}

非矢量化代码:

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;
}

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

   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   

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

enter image description here

2 个答案:

答案 0 :(得分:1)

正如您在一些评论中所看到的,缓存未命中来自性能的提升。

例如,对于最近的CPU,您可以在每个周期执行2个AVX2 add或mul,因此每个周期可以执行512位。您需要加载数据的时间会更长,因为它需要多个缓存行。

此外,根据您的系统配置方式,超线程,亲和力等,您的调度程序可以在使用其他线程/进程污染缓存的同时执行其他操作。

最后一件事。 CPU现在非常有效地将简单模式识别为具有非常小的循环的模式,然后在几次迭代后自动使用预取。无论如何,它还不足以解决缓存大小问题。

尝试使用不同大小的N,你应该看到有趣的结果。 此外,首先对齐数据,并确保如果使用2个变量,则不会共享相同的缓存行。

答案 1 :(得分:0)

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

“ - O2优化得更多.GCC执行几乎所有支持的优化,不涉及空速交换。” (从这里:https://gcc.gnu.org/onlinedocs/gcc/Optimize-Options.html

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

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

原因可能是两个因素的结合。

首先,更快的代码在相同的时间内向内存/缓存生成更多请求,这强调了预取预测算法。 L1缓存不是很大,通常为1MB - 3MB,并且在该CPU Core上的所有正在运行的进程之间共享,因此在先前预取的块不再使用之前,CPU Core无法预取。如果代码运行得更快,则在块之间预取的时间更短,并且在有效管道的代码中,在CPU Core完全停止之前将执行更多的缓存未命中,直到挂起的提取完成。

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