问题
我正在研究高性能矩阵乘法算法,如OpenBLAS或GotoBLAS,我正在尝试重现一些结果。这个问题涉及矩阵乘法算法的内核。具体来说,我正在考虑计算C += AB
,其中A
和B
是我的CPU峰值速度类型为double
的2x2矩阵。有两种方法可以做到这一点。一种方法是使用SIMD指令。第二种方法是使用SIMD寄存器直接在汇编代码中编码。
到目前为止我看过的内容
所有相关论文,课程网页,许多SO Q& As处理主题(太多无法列出),我在我的计算机上编译了OpenBLAS,查看了OpenBLAS,GotoBLAS和BLIS源代码,Agner&#39 ; s手册。
硬件
我的CPU是Intel i5 - 540M。您可以在cpu-world.com上找到相关的CPUID信息。微体系结构是Nehalem(westmere),因此理论上每循环每个核心可以计算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;
}
我使用朴素矩阵矩阵乘法(如下所示)计算一个真正的答案,它允许我通过视觉或通过计算所有元素的L2范数来检查我的结果
// "true" answer
for(int i = 0; i < n; i++)
for(int j = 0; j < n; j++)
for(int k = 0; k < n; k++)
T[i*n + j] += A[i*n + k]*B[k*n + j];
要运行代码并获得Gflops的估计值,我将每个乘法函数调用一次以进行预热,然后在for
循环内执行maxiter
次,确保将{0}调零每当我计算C
时,{1}}矩阵。 C += AB
循环放在两个for
语句中,用于估算Gflops。代码片段说明了这一部分。
clock()
SIMD代码
我的CPU支持128位向量,因此我可以在每个向量中加入2 C[0] = 0.0; C[1] = 0.0; C[2] = 0.0; C[3] = 0.0;
mult2by2(A,B,C); //warmup
time1 = clock();
for(int i = 0; i < maxiter; i++){
mult2by2(A,B,C);
C[0] = 0.0; C[1] = 0.0; C[2] = 0.0; C[3] = 0.0;
}
time2 = clock() - time1;
time3 = (double)(time2)/CLOCKS_PER_SEC;
gflops = (double) (2.0*n*n*n)/time3/1.0e9*maxiter;
mult2by2(A,B,C); // to compute the norm against T
norm = L2norm(n,C,T);
s。这是我在内核中进行2x2矩阵乘法的主要原因。 SIMD代码一次计算整行double
。
C
Assmebly(英特尔语法)
我的第一次尝试是为此部件创建一个单独的装配例程,并从 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);
}
例程调用它。但是,它非常慢,因为我无法内联main
函数。我将程序集编写为内联汇编,如下所示。它与extern
生成的相同。根据我对Nehalem微体系结构图的理解,此处理器可以并行执行gcc -S -std=c99 -O3 -msse3 -ffast-math -march=nocona -mtune=nocona -funroll-all-loops -fomit-frame-pointer -masm=intel
,SSE ADD
和SSE MUL
,这解释了SSE MOV
,{{1}的交错},MUL
说明。您会注意到上面的SIMD说明顺序不同,因为我对Agner Fog手册有不同的理解。尽管如此,ADD
很聪明,上面的SIMD代码编译成内联版本中显示的程序集。
MOV
结果
我用以下标志编译我的代码:
gcc
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
如果我强制SIMD版本不与maxiter = 1000000000
内联,则结果为:
********** 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
问题
如果内联ASM和SIMD实现产生相同的汇编输出,为什么汇编版本要慢得多?这就好像内联汇编没有内联一样,第二组结果显示了内联汇编的相同性能。#34; inline&#34; ASM与&#34; noinline&#34; SIMD。我能找到的唯一解释是 Agner Fog第2卷第6页:
编译代码可能比汇编代码更快,因为编译器可以编写代码 程序间优化和整个程序优化。大会 程序员通常必须使用定义良好的调用来创建定义良好的函数 遵循所有调用约定的接口,以使代码可测试和 验证。这可以防止编译器使用的许多优化方法,例如 作为函数内联,寄存器分配,常量传播,公共子表达式 跨功能消除,跨功能调度等。这些 使用具有内部函数的C ++代码而不是使用C ++代码可以获得优势 汇编代码。
但两个版本的汇编输出完全相同。
为什么我在第一组结果中看到44 Gflops?这远高于我计算的12 Gflops峰值,如果我使用单精度计算运行这两个核心,那就是我所期望的。
编辑1
评论说可能存在死代码消除我可以确认SIMd指令正在发生这种情况。 __attribute__ ((noinline))
输出显示SIMD的********** 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
循环仅为零-S
矩阵。我可以通过使用for
关闭编译器优化来禁用它。在这种情况下,SIMD的运行速度是ASM的3倍,但ASM仍以完全相同的速度运行。规范现在也非零,但它在10 ^ -16仍然可以。我还看到内联ASM版本内嵌了C
和-O0
标记,但它也在APP
循环中展开了8次。我认为多次展开会严重影响性能,因为我通常会循环4次循环。根据我的经验,更多的东西似乎会降低性能。
答案 0 :(得分:3)
GCC正在使用内在函数mult2by2B
优化您的内联函数,因为该行
C[0] = 0.0; C[1] = 0.0; C[2] = 0.0; C[3] = 0.0;
没有那条线,Coliru的电脑需要2.9秒 http://coliru.stacked-crooked.com/a/992304f5f672e257
使用该行只需0.000001 http://coliru.stacked-crooked.com/a/9722c39bb6b8590a
您也可以在程序集中看到这一点。如果您将下面的代码放到http://gcc.godbolt.org/中,您会看到使用该行代码完全跳过该函数。
但是,当你内联程序集GCC没有优化函数mult2by2A
时,即使它内联它(即使它内联它)。您也可以在程序集中看到这一点。
#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);
}