我正在尝试学习如何利用gcc利用矢量化。我按照Erik Holk(source 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;
}
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 )
答案 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)个点积,以获得双浮点。