Warning: file_get_contents(/data/phpspider/zhask/data//catemap/4/c/55.json): failed to open stream: No such file or directory in /data/phpspider/zhask/libs/function.php on line 167

Warning: Invalid argument supplied for foreach() in /data/phpspider/zhask/libs/tag.function.php on line 1116

Notice: Undefined index: in /data/phpspider/zhask/libs/function.php on line 180

Warning: array_chunk() expects parameter 1 to be array, null given in /data/phpspider/zhask/libs/function.php on line 181

Warning: file_get_contents(/data/phpspider/zhask/data//catemap/5/date/2.json): failed to open stream: No such file or directory in /data/phpspider/zhask/libs/function.php on line 167

Warning: Invalid argument supplied for foreach() in /data/phpspider/zhask/libs/tag.function.php on line 1116

Notice: Undefined index: in /data/phpspider/zhask/libs/function.php on line 180

Warning: array_chunk() expects parameter 1 to be array, null given in /data/phpspider/zhask/libs/function.php on line 181
C 优化的2x2矩阵乘法:慢速组装与快速SIMD_C_Assembly_Matrix - Fatal编程技术网

C 优化的2x2矩阵乘法:慢速组装与快速SIMD

C 优化的2x2矩阵乘法:慢速组装与快速SIMD,c,assembly,matrix,C,Assembly,Matrix,问题 我正在研究高性能的矩阵乘法算法,比如OpenBLAS或GotoBLAS,我试图重现一些结果。这个问题涉及矩阵乘法算法的内核。具体来说,我正在研究计算C+=AB,其中A和B是double类型的2x2矩阵,以CPU的峰值速度。有两种方法可以做到这一点。一种方法是使用SIMD指令。第二种方法是使用SIMD寄存器在汇编中直接编码 到目前为止我所看到的 所有相关的论文,课程网页,许多关于这个主题的问答(太多了,无法列出),我在我的电脑上编译了OpenBLAS,查阅了OpenBLAS,GotoBLAS

问题

我正在研究高性能的矩阵乘法算法,比如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
问题

  • 如果内联ASM和SIMD实现都产生相同的程序集输出,为什么程序集版本会慢得多?这就好像内联程序集没有内联,第二组结果表明“内联”ASM与“noinline”SIMD的性能相同,这一点很明显。我能找到的唯一解释是Agner Fog第2卷第6页:

    编译后的代码可能比汇编代码快,因为编译器可以 程序间优化和全程序优化。大会 程序员通常必须通过定义良好的调用来生成定义良好的函数 接口,该接口遵守所有调用约定,以使代码可测试且 可验证的。这防止了编译器使用的许多优化方法,例如 as函数内联、寄存器分配、常量传播、公共子表达式 跨功能消除、跨功能调度等。这些 通过使用具有内部功能的C++代码来获得优点 汇编代码

    但两个版本的汇编程序输出完全相同

  • 为什么我在第一组结果中看到44亿次?这远远高于我计算的12gflops峰值,如果我用单精度计算运行两个内核,这就是我所期望的

  • 编辑1
    评论说可能会有死代码消除,我可以确认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);
    }