【问题标题】:Improving the performance of Matrix Multiplication提高矩阵乘法的性能
【发布时间】:2017-11-06 14:08:15
【问题描述】:

这是我的加速矩阵乘法的代码,但它只比简单的快 5%。 我能做些什么来尽可能地提高它?

*这些表被访问例如:C[sub2ind(i,j,n)] 用于 C[i, j] 位置。 p>

void matrixMultFast(float * const C,            /* output matrix */
                float const * const A,      /* first matrix */
                float const * const B,      /* second matrix */
                int const n,                /* number of rows/cols */
                int const ib,               /* size of i block */
                int const jb,               /* size of j block */
                int const kb)               /* size of k block */
{

int i=0, j=0, jj=0, k=0, kk=0;
float sum;

for(i=0;i<n;i++)
    for(j=0;j<n;j++)
        C[sub2ind(i,j,n)]=0;

for(kk=0;kk<n;kk+=kb)
{
    for(jj=0;jj<n;jj+=jb)
    {
        for(i=0;i<n;i++)
        {
            for(j=jj;j<jj+jb;j++)
            {
                sum=C[sub2ind(i,j,n)];
                for(k=kk;k<kk+kb;k++)
                    sum += A[sub2ind(i,k,n)]*B[sub2ind(k,j,n)];
                C[sub2ind(i,j,n)]=sum;
            }
        }
    }
}
} // end function 'matrixMultFast4'

*C语言编写,需要支持C99

【问题讨论】:

  • 您可以使用 SIMD 扩展来加快速度。
  • 有一些很好的软件可以为您执行此操作,称为 BLAS(基本线性代数子程序),您正在寻找的例程称为 DGEMM(但还有很多其他针对特定的优化类型的矩阵乘法)。 GotoBLAS、OpenBLAS、英特尔的 MKL、AMD 的 ACML 等等。如果你真的想加速你自己的版本,你需要使用矢量化和并行化。
  • 转置您的输入之一,以便您可以访问一个矩阵的行和另一个矩阵的列作为连续内存。您的B[sub2ind(k,j,n)] 是问题所在,因为它在内部循环的每次迭代中都跨越n,给您带来糟糕的缓存访问局部性。如果你解决了这个问题,那么你尝试缓存阻塞可能会有很大帮助。另外,只需使用memsetC[] 归零即可。

标签: c matrix matrix-multiplication c99


【解决方案1】:

您可以做很多很多事情来提高矩阵乘法的效率。

为了研究如何改进基本算法,让我们先来看看我们目前的选择。当然,简单的实现有 3 个循环,时间复杂度为 O(n^3)。还有另一种方法称为 Strassen 方法,它实现了明显的加速并且具有 O(n^2.73) 的顺序(但实际上是无用的,因为它没有提供明显的优化方法)。

这是理论上的。现在考虑矩阵是如何存储在内存中的。行专业是标准,但您也可以找到列专业。根据方案,转置矩阵可能会因为缓存未命中次数减少而提高速度。理论上的矩阵乘法只是一堆向量点积和加法。同一个向量将被多个向量操作,因此将该向量保存在缓存中以便更快地访问是有意义的。

现在,随着多核、并行性和缓存概念的引入,游戏规则发生了变化。如果我们仔细观察,我们会发现点积只不过是一堆乘法,然后是求和。这些乘法可以并行完成。因此,我们现在可以查看数字的并行加载。

现在让我们让事情变得更复杂一些。在谈论矩阵乘法时,单浮点和双浮点在大小上是有区别的。通常前者是 32 位,而后者是 64 位(当然,这取决于 CPU)。每个 CPU 只有固定数量的寄存器,这意味着你的数字越大,你在 CPU 中的容量就越小。故事的寓意是,除非您真的需要双精度,否则请坚持使用单浮点。

现在我们已经了解了如何调整矩阵乘法的基础知识,不用担心。您不需要执行上面讨论的任何操作,因为已经有子例程可以执行此操作。如 cmets 中所述,有 GotoBLAS、OpenBLAS、Intel 的 MKL 和 Apple 的 Accelerate 框架。 MKL/Accelerate 是专有的,但 OpenBLAS 是一个非常有竞争力的替代方案。

这是一个很好的小例子,它在我的 Macintosh 上在几毫秒内将 2 个 8k x 8k 矩阵相乘:

#include <sys/time.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <Accelerate/Accelerate.h>

int SIZE = 8192;

typedef float point_t;

point_t* transpose(point_t* A) {    
    point_t* At = (point_t*) calloc(SIZE * SIZE, sizeof(point_t));    
    vDSP_mtrans(A, 1, At, 1, SIZE, SIZE);

    return At;
}

point_t* dot(point_t* A, point_t* B) {
    point_t* C = (point_t*) calloc(SIZE * SIZE, sizeof(point_t));       
    int i;    
    int step = (SIZE * SIZE / 4);

    cblas_sgemm (CblasRowMajor, 
       CblasNoTrans, CblasNoTrans, SIZE/4, SIZE, SIZE,
       1.0, &A[0], SIZE, B, SIZE, 0.0, &C[0], SIZE);

    cblas_sgemm (CblasRowMajor, 
       CblasNoTrans, CblasNoTrans, SIZE/4, SIZE, SIZE,
       1.0, &A[step], SIZE, B, SIZE, 0.0, &C[step], SIZE);

    cblas_sgemm (CblasRowMajor, 
       CblasNoTrans, CblasNoTrans, SIZE/4, SIZE, SIZE,
       1.0, &A[step * 2], SIZE, B, SIZE, 0.0, &C[step * 2], SIZE);

    cblas_sgemm (CblasRowMajor, 
       CblasNoTrans, CblasNoTrans, SIZE/4, SIZE, SIZE,
       1.0, &A[step * 3], SIZE, B, SIZE, 0.0, &C[step * 3], SIZE);      

    return C;
}

void print(point_t* A) {
    int i, j;
    for(i = 0; i < SIZE; i++) {
        for(j = 0; j < SIZE; j++) {
            printf("%f  ", A[i * SIZE + j]);
        }
        printf("\n");
    }
}

int main() {
    for(; SIZE <= 8192; SIZE *= 2) {
        point_t* A = (point_t*) calloc(SIZE * SIZE, sizeof(point_t));
        point_t* B = (point_t*) calloc(SIZE * SIZE, sizeof(point_t));

        srand(getpid());

        int i, j;
        for(i = 0; i < SIZE * SIZE; i++) {
            A[i] = ((point_t)rand() / (double)RAND_MAX);
            B[i] = ((point_t)rand() / (double)RAND_MAX);
        }

        struct timeval t1, t2;
        double elapsed_time;

        gettimeofday(&t1, NULL);
        point_t* C = dot(A, B);
        gettimeofday(&t2, NULL);

        elapsed_time = (t2.tv_sec - t1.tv_sec) * 1000.0;      // sec to ms
        elapsed_time += (t2.tv_usec - t1.tv_usec) / 1000.0;   // us to ms

        printf("Time taken for %d size matrix multiplication: %lf\n", SIZE, elapsed_time/1000.0);

        free(A);
        free(B);
        free(C);

    }
    return 0;
}

在这一点上,我还应该提到 SSE(流 SIMD 扩展),这基本上是您不应该做的事情,除非您使用过汇编。基本上,您正在向量化您的 C 代码,以使用向量而不是整数。这意味着您可以对数据块而不是单个值进行操作。编译器放弃并只是按原样翻译您的代码,而不进行自己的优化。如果处理得当,它可以前所未有地加速你的代码——你甚至可以触及O(n^2) 的理论底线!但是很容易滥用 SSE,不幸的是大多数人都这样做了,这使得最终结果比以前更糟。

我希望这能激励您深入挖掘。矩阵乘法的世界是一个庞大而迷人的世界。下面,我附上链接以供进一步阅读。

  1. OpenBLAS
  2. More about SSE
  3. Intel Intrinsics

【讨论】:

  • 谢谢!我的问题是我不能使用其他库,这是一个将在另一台计算机上编译的练习。我可以使用 SIMD(并且我使用过汇编),但我不知道如何使用它。
  • 如果您使用大型矩阵,则始终可以选择转置和/或分解矩阵。是的,SIMD 不适合胆小的人。
猜你喜欢
  • 2013-09-06
  • 1970-01-01
  • 1970-01-01
  • 2015-02-03
  • 2017-07-05
  • 2019-01-02
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多