手动矢量化代码比自动优化慢10倍 - 我做错了什么?

时间:2014-03-11 08:59:54

标签: gcc optimization scipy vectorization

我正在尝试学习如何利用gcc利用矢量化。我按照Erik Holksource code here

的教程进行了操作

我刚修改它加倍。我用这个dotproduct来计算随机生成的方形矩阵1200x1200的双倍乘法(300x300 double4)。我检查结果是一样的。但让我感到惊讶的是,简单的 dotproduct实际上比我的手动矢量化快了10倍。

也许,double4对于SSE来说太大了(它需要AVX2?)但是我希望即使在gcc找不到合适的指令来同时处理double4的情况下,它仍然可以利用显式信息数据是大块的自动矢量化。


详细信息:

结果是:

dot_simple:
time elapsed 1.90000 [s] for 1.728000e+09 evaluations => 9.094737e+08 [ops/s]

dot_SSE:
time elapsed 15.78000 [s] for 1.728000e+09 evaluations => 1.095057e+08 [ops/s]

我使用gcc 4.6.3在英特尔®酷睿™i5 CPU 750 @ 2.67GHz×4上使用这些选项-std=c99 -O3 -ftree-vectorize -unroll-loops --param max-unroll-times=4 -ffast-math或仅使用-O2 (结果是一样的)

为方便起见,我使用python / scipy.weave()完成了它,但我希望它不会改变任何东西

代码:

double dot_simple(  int n, double *a, double *b ){
    double dot = 0;
    for (int i=0; i<n; i++){ 
        dot += a[i]*b[i];
    }
    return dot;
}

并明确使用gcc vector extensiobns

double dot_SSE(  int n, double *a, double *b ){
    const int VECTOR_SIZE = 4;
    typedef double double4 __attribute__ ((vector_size (sizeof(double) * VECTOR_SIZE)));
    double4 sum4 = {0};
    double4* a4 = (double4 *)a;
    double4* b4 = (double4 *)b;
    for (int i=0; i<n; i++){ 
        sum4 += *a4 * *b4 ;
        a4++; b4++;
        //sum4 += a4[i] * b4[i];
    }
    union {  double4 sum4_; double sum[VECTOR_SIZE]; };
    sum4_ = sum4;
    return sum[0]+sum[1]+sum[2]+sum[3];
}

然后我用它来乘以300x300随机矩阵来衡量性能

void mmul( int n, double* A, double* B, double* C ){
    int n4 = n*4;
    for (int i=0; i<n4; i++){
        for (int j=0; j<n4; j++){
            double* Ai = A + n4*i;
            double* Bj = B + n4*j;
            C[ i*n4 + j ] =  dot_SSE( n, Ai, Bj );
            //C[ i*n4 + j ] =  dot_simple( n4, Ai, Bj );
            ijsum++;
        }
    }
}

scipy编织代码:

def mmul_2(A, B, C, __force__=0 ):
    code = r'''     mmul( NA[0]/4, A, B, C );            '''
    weave_options = {
    'extra_compile_args': ['-std=c99 -O3 -ftree-vectorize -unroll-loops --param max-unroll-times=4 -ffast-math'],
    'compiler' : 'gcc', 'force' : __force__ }
    return weave.inline(code, ['A','B','C'], verbose=3, headers=['"vectortest.h"'],include_dirs=['.'], **weave_options )

1 个答案:

答案 0 :(得分:1)

主要问题之一是,在你的函数dot_SSE中,当你应该只循环n / 2项(或用AVX的n / 4)时,你循环遍历n个项目。

要使用GCC的矢量扩展来解决此问题,您可以执行以下操作:

double dot_double2(int n, double *a, double *b ) {
    typedef double double2 __attribute__ ((vector_size (16)));
    double2 sum2 = {};
    int i;
    double2* a2 = (double2*)a;
    double2* b2 = (double2*)b;
    for(i=0; i<n/2; i++) {
        sum2 += a2[i]*b2[i];
    }
    double dot = sum2[0] + sum2[1];
    for(i*=2;i<n; i++) dot +=a[i]*b[i]; 
    return dot;
}

您的代码的另一个问题是它有一个依赖链。您的CPU可以同时进行SSE添加和乘法,但仅适用于独立的数据路径。要解决此问题,您需要展开循环。以下代码将循环展开2(但您可能需要展开3才能获得最佳结果)。

double dot_double2_unroll2(int n, double *a, double *b ) {
    typedef double double2 __attribute__ ((vector_size (16)));
    double2 sum2_v1 = {};
    double2 sum2_v2 = {};
    int i;
    double2* a2 = (double2*)a;
    double2* b2 = (double2*)b;
    for(i=0; i<n/4; i++) {       
        sum2_v1 += a2[2*i+0]*b2[2*i+0];
        sum2_v2 += a2[2*i+1]*b2[2*i+1];
    }
    double dot = sum2_v1[0] + sum2_v1[1] + sum2_v2[0] + sum2_v2[1];
    for(i*=4;i<n; i++) dot +=a[i]*b[i]; 
    return dot;
}

这是一个使用double4的版本,我认为这正是您想要的原始dot_SSE功能。它是AVX的理想选择(尽管它仍然需要展开),但它仍然适用于SSE2。事实上,对于SSE,似乎GCC将其分成两个链,这有效地将循环展开2个。

double dot_double4(int n, double *a, double *b ) {
    typedef double double4 __attribute__ ((vector_size (32)));
    double4 sum4 = {};
    int i;
    double4* a4 = (double4*)a;
    double4* b4 = (double4*)b;
    for(i=0; i<n/4; i++) {       
        sum4 += a4[i]*b4[i];
    }
    double dot = sum4[0] + sum4[1] + sum4[2] + sum4[3];
    for(i*=4;i<n; i++) dot +=a[i]*b[i]; 
    return dot;
}

如果使用FMA编译它,它将生成FMA3指令。我在这里测试了所有这些函数(您也可以自己编辑和编译代码)http://coliru.stacked-crooked.com/a/273268902c76b116

请注意,在矩阵乘法中使用SSE / AVX进行单点生成并不是SIMD的最佳用途。您应该使用SSE(AVX)一次做两(4)个点积,以获得双浮点。