C 优化的2x2矩阵乘法:慢速组装与快速SIMD
问题 我正在研究高性能的矩阵乘法算法,比如OpenBLAS或GotoBLAS,我试图重现一些结果。这个问题涉及矩阵乘法算法的内核。具体来说,我正在研究计算C 优化的2x2矩阵乘法:慢速组装与快速SIMD,c,assembly,matrix,C,Assembly,Matrix,问题 我正在研究高性能的矩阵乘法算法,比如OpenBLAS或GotoBLAS,我试图重现一些结果。这个问题涉及矩阵乘法算法的内核。具体来说,我正在研究计算C+=AB,其中A和B是double类型的2x2矩阵,以CPU的峰值速度。有两种方法可以做到这一点。一种方法是使用SIMD指令。第二种方法是使用SIMD寄存器在汇编中直接编码 到目前为止我所看到的 所有相关的论文,课程网页,许多关于这个主题的问答(太多了,无法列出),我在我的电脑上编译了OpenBLAS,查阅了OpenBLAS,GotoBLAS
C+=AB
,其中A
和B
是double
类型的2x2矩阵,以CPU的峰值速度。有两种方法可以做到这一点。一种方法是使用SIMD指令。第二种方法是使用SIMD寄存器在汇编中直接编码
到目前为止我所看到的
所有相关的论文,课程网页,许多关于这个主题的问答(太多了,无法列出),我在我的电脑上编译了OpenBLAS,查阅了OpenBLAS,GotoBLAS和BLIS源代码,Agner的手册
硬件
我的CPU是Intel i5-540M。您可以在cpu-world.com上找到相关的CPUID信息。微体系结构是Nehalem(Westmile),因此理论上它可以计算每个核心每个周期4个双精度触发器。我将只使用一个内核(无OpenMP),因此,使用超线程关闭和4步Intel Turbo Boost,我将看到峰值(2.533 Ghz+4*0.133 Ghz)*(4 DP flops/core/cycle)*(1 core)=12.27 DP Gflops
。作为参考,由于两个内核都在峰值运行,Intel Turbo Boost提供了两步加速,理论峰值应为22.4 DP Gflops
设置
我将我的2x2矩阵声明为double
,并使用随机条目初始化它们,如下面的代码片段所示
srand(time(NULL));
const int n = 2;
double A[n*n];
double B[n*n];
double C[n*n];
double T[n*n];
for(int i = 0; i < n*n; i++){
A[i] = (double) rand()/RAND_MAX;
B[i] = (double) rand()/RAND_MAX;
C[i] = 0.0;
}
SIMD代码
我的CPU支持128位向量,因此我可以在每个向量中放入2个double
s。这就是我在内核中进行2x2矩阵乘法的主要原因。SIMD代码一次计算一整行C
inline void
__attribute__ ((gnu_inline))
__attribute__ ((aligned(16))) mult2by2B(
const double* restrict A,
const double* restrict B,
double* restrict C
)
{
register __m128d xmm0, xmm1, xmm2, xmm3, xmm4;
xmm0 = _mm_load_pd(C);
xmm1 = _mm_load1_pd(A);
xmm2 = _mm_load_pd(B);
xmm3 = _mm_load1_pd(A + 1);
xmm4 = _mm_load_pd(B + 2);
xmm1 = _mm_mul_pd(xmm1,xmm2);
xmm2 = _mm_add_pd(xmm1,xmm0);
xmm1 = _mm_mul_pd(xmm3,xmm4);
xmm2 = _mm_add_pd(xmm1,xmm2);
_mm_store_pd(C,xmm2);
xmm0 = _mm_load_pd(C + 2);
xmm1 = _mm_load1_pd(A + 2);
xmm2 = _mm_load_pd(B);
xmm3 = _mm_load1_pd(A + 3);
//xmm4 = _mm_load_pd(B + 2);
xmm1 = _mm_mul_pd(xmm1,xmm2);
xmm2 = _mm_add_pd(xmm1,xmm0);
xmm1 = _mm_mul_pd(xmm3,xmm4);
xmm2 = _mm_add_pd(xmm1,xmm2);
_mm_store_pd(C + 2,xmm2);
}
Assmebly(英特尔语法)
我的第一次尝试是为该零件创建一个单独的组装例程,并从main
例程调用它。但是,它非常慢,因为我无法内联extern
函数。我将程序集编写为内联程序集,如下所示。它与gcc-S-std=c99-O3-msse3-ffast math-march=nocona-mtune=nocona-funroll all loops-fomit frame pointer-masm=intel生成的相同。根据我对Nehalem微体系结构图的理解,该处理器可以并行执行SSE ADD
、SSE MUL
和SSE MOV
,这解释了MUL
、ADD
、MOV
指令的交错。您会注意到上面的SIMD说明顺序不同,因为我对Agner Fog手册的理解不同。然而,gcc
是智能的,上面的SIMD代码编译成内联版本中显示的程序集
inline void
__attribute__ ((gnu_inline))
__attribute__ ((aligned(16))) mult2by2A
(
const double* restrict A,
const double* restrict B,
double* restrict C
)
{
__asm__ __volatile__
(
"mov edx, %[A] \n\t"
"mov ecx, %[B] \n\t"
"mov eax, %[C] \n\t"
"movapd xmm3, XMMWORD PTR [ecx] \n\t"
"movapd xmm2, XMMWORD PTR [ecx+16] \n\t"
"movddup xmm1, QWORD PTR [edx] \n\t"
"mulpd xmm1, xmm3 \n\t"
"addpd xmm1, XMMWORD PTR [eax] \n\t"
"movddup xmm0, QWORD PTR [edx+8] \n\t"
"mulpd xmm0, xmm2 \n\t"
"addpd xmm0, xmm1 \n\t"
"movapd XMMWORD PTR [eax], xmm0 \n\t"
"movddup xmm4, QWORD PTR [edx+16] \n\t"
"mulpd xmm4, xmm3 \n\t"
"addpd xmm4, XMMWORD PTR [eax+16] \n\t"
"movddup xmm5, QWORD PTR [edx+24] \n\t"
"mulpd xmm5, xmm2 \n\t"
"addpd xmm5, xmm4 \n\t"
"movapd XMMWORD PTR [eax+16], xmm5 \n\t"
: // no outputs
: // inputs
[A] "m" (A),
[B] "m" (B),
[C] "m" (C)
: //register clobber
"memory",
"edx","ecx","eax",
"xmm0","xmm1","xmm2","xmm3","xmm4","xmm5"
);
}
结果
我使用以下标志编译代码:
gcc -std=c99 -O3 -msse3 -ffast-math -march=nocona -mtune=nocona -funroll-all-loops -fomit-frame-pointer -masm=intel
maxiter=100000000
的结果如下:
********** Inline ASM
L2 norm: 0.000000e+000, Avg. CPU time: 9.563000, Avg. Gflops: 1.673115
********** SIMD Version
L2 norm: 0.000000e+000, Avg. CPU time: 0.359000, Avg. Gflops: 44.568245
如果我强制SIMD版本不与\uuuuu属性((noinline))
内联,结果如下:
********** Inline ASM
L2 norm: 0.000000e+000, Avg. CPU time: 11.155000, Avg. Gflops: 1.434334
********** SIMD Version
L2 norm: 0.000000e+000, Avg. CPU time: 11.264000, Avg. Gflops: 1.420455
问题
评论说可能会有死代码消除,我可以确认SIMd指令正在发生这种情况。
-S
输出显示,SIMD的for
循环仅为零C
矩阵。我可以通过使用-O0
关闭编译器优化来禁用它。在这种情况下,SIMD的运行速度是ASM的3倍,但ASM仍然以完全相同的速度运行。范数现在也非零,但在10^-16时仍然可以。我还看到内联ASM版本与APP
和NO_APP
标记内联,但它也在for
循环中展开了8次。我认为多次展开会严重影响性能,因为我通常会展开循环4次。根据我的经验,更多的东西似乎会降低性能。GCC正在使用内部函数优化内联函数,mult2by2B
,因为
C[0] = 0.0; C[1] = 0.0; C[2] = 0.0; C[3] = 0.0;
如果没有这条线路,科里鲁在电脑上只需要2.9秒
这条线只需要0.000001
您也可以在部件中看到这一点。如果您将下面的代码放入,您将看到,使用这行代码,它完全跳过了函数
但是,当您内联程序集时,GCC并没有优化函数,mult2by2A
,否则(
********** Inline ASM
L2 norm: 0.000000e+000, Avg. CPU time: 11.155000, Avg. Gflops: 1.434334
********** SIMD Version
L2 norm: 0.000000e+000, Avg. CPU time: 11.264000, Avg. Gflops: 1.420455
C[0] = 0.0; C[1] = 0.0; C[2] = 0.0; C[3] = 0.0;
#include <stdio.h>
#include <emmintrin.h> // SSE2
#include <omp.h>
inline void
__attribute__ ((gnu_inline))
__attribute__ ((aligned(16))) mult2by2B(
const double* __restrict A,
const double* __restrict B,
double* __restrict C
)
{
register __m128d xmm0, xmm1, xmm2, xmm3, xmm4;
xmm0 = _mm_load_pd(C);
xmm1 = _mm_load1_pd(A);
xmm2 = _mm_load_pd(B);
xmm3 = _mm_load1_pd(A + 1);
xmm4 = _mm_load_pd(B + 2);
xmm1 = _mm_mul_pd(xmm1,xmm2);
xmm2 = _mm_add_pd(xmm1,xmm0);
xmm1 = _mm_mul_pd(xmm3,xmm4);
xmm2 = _mm_add_pd(xmm1,xmm2);
_mm_store_pd(C,xmm2);
xmm0 = _mm_load_pd(C + 2);
xmm1 = _mm_load1_pd(A + 2);
xmm2 = _mm_load_pd(B);
xmm3 = _mm_load1_pd(A + 3);
//xmm4 = _mm_load_pd(B + 2);
xmm1 = _mm_mul_pd(xmm1,xmm2);
xmm2 = _mm_add_pd(xmm1,xmm0);
xmm1 = _mm_mul_pd(xmm3,xmm4);
xmm2 = _mm_add_pd(xmm1,xmm2);
_mm_store_pd(C + 2,xmm2);
}
int main() {
double A[4], B[4], C[4];
int maxiter = 10000000;
//int maxiter = 1000000000;
double dtime;
dtime = omp_get_wtime();
for(int i = 0; i < maxiter; i++){
mult2by2B(A,B,C);
C[0] = 0.0; C[1] = 0.0; C[2] = 0.0; C[3] = 0.0;
}
dtime = omp_get_wtime() - dtime;
printf("%f %f %f %f\n", C[0], C[1], C[2], C[3]);
//gflops = (double) (2.0*n*n*n)/time3/1.0e9*maxiter;
printf("time %f\n", dtime);
}