为什么MATLAB在矩阵乘法中如此之快?

时间:2011-05-19 11:46:59

标签: matlab performance cuda matrix-multiplication

我正在使用CUDA,C ++,C#和Java制作一些基准测试,并使用MATLAB进行验证和矩阵生成。但是当我乘以MATLAB时,2048x2048甚至更大的矩阵几乎立即成倍增加。

             1024x1024   2048x2048   4096x4096
             ---------   ---------   ---------
CUDA C (ms)      43.11      391.05     3407.99
C++ (ms)       6137.10    64369.29   551390.93
C# (ms)       10509.00   300684.00  2527250.00
Java (ms)      9149.90    92562.28   838357.94
MATLAB (ms)      75.01      423.10     3133.90

只有CUDA才具有竞争力,但我认为至少C ++会有点接近而不会60x慢。

所以我的问题是 - MATLAB如何快速地完成它?

C ++代码:

float temp = 0;
timer.start();
for(int j = 0; j < rozmer; j++)
{
    for (int k = 0; k < rozmer; k++)
    {
        temp = 0;
        for (int m = 0; m < rozmer; m++)
        {
            temp = temp + matice1[j][m] * matice2[m][k];
        }
        matice3[j][k] = temp;
    }
}
timer.stop();

编辑: 我也不知道如何考虑C#的结果。该算法与C ++和Java相同,但2048有一个巨大的跳跃1024

EDIT2: 更新了MATLAB和4096x4096结果

11 个答案:

答案 0 :(得分:160)

这种问题反复出现,应该比#34更清晰地回答; Matlab使用高度优化的库&#34;或者&#34; Matlab使用MKL&#34;在Stackoverflow上一次。

<强>历史:

矩阵乘法(与矩阵向量,向量 - 向量乘法和许多矩阵分解一起)是线性algrebra中最重要的问题。从早期开始,工程师一直在用计算机解决这些问题。

我不是历史专家,但显然那时候,每个人都只用简单的循环重写了他的Fortran版本。然后出现了一些标准化,并确定了&#34;内核&#34; (基本程序)大多数线性代数问题需要解决。然后将这些基本操作标准化为:基本线性代数子程序(BLAS)。然后,工程师可以在他们的代码中调用这些经过良好测试的标准BLAS例程,使他们的工作更加轻松。

<强> BLAS:

BLAS从1级(定义标量矢量和矢量矢量运算的第一个版本)演变为2级(矢量矩阵运算)到3级(矩阵矩阵运算),并提供越来越多的&#34 ;内核&#34;因此标准化了越来越多的基本线性代数运算。最初的Fortran 77实现仍可在Netlib's website上使用。

迈向更好的表现:

多年来(特别是在BLAS 1级和2级版本之间:80年代早期),随着矢量操作和缓存层次结构的出现,硬件发生了变化。这些演进使得有可能大大提高BLAS子程序的性能。然后,不同的供应商提出了BLAS例程的实现,这些例程越来越高效。

我不知道所有的历史实现(当时我还没出生或是个孩子),但是最着名的两个最早出现在21世纪初:英特尔MKL和GotoBLAS。您的Matlab使用的是英特尔MKL,这是一款非常优秀的优化BLAS,它解释了您所看到的卓越性能。

Matrix乘法的技术细节:

那么为什么Matlab(MKL)如此快dgemm(双精度一般矩阵 - 矩阵乘法)?简单来说:因为它使用矢量化和良好的数据缓存。更复杂的术语:请参阅Jonathan Moore提供的article

基本上,当您在所提供的C ++代码中执行乘法运算时,您根本不熟悉缓存。由于我怀疑你创建了一个指向行数组的指针数组,因此你在内部循环中访问&#34; matice2&#34;:matice2[m][k]的第k列非常慢。实际上,当您访问matice2[0][k]时,您必须获得矩阵的数组0的第k个元素。然后在下一次迭代中,您必须访问matice2[1][k],这是另一个数组(数组1)的第k个元素。然后在下一次迭代中,您访问另一个数组,依此类推......因为整个矩阵matice2不能适应最高的缓存(它的8*1024*1024字节大) ,程序必须从主内存中获取所需的元素,这会浪费很多时间。

如果你只是转换了矩阵,那么访问将在连续的内存地址中,你的代码已经运行得更快,因为现在编译器可以同时加载缓存中的整行。试试这个修改过的版本:

timer.start();
float temp = 0;
//transpose matice2
for (int p = 0; p < rozmer; p++)
{
    for (int q = 0; q < rozmer; q++)
    {
        tempmat[p][q] = matice2[q][p];
    }
}
for(int j = 0; j < rozmer; j++)
{
    for (int k = 0; k < rozmer; k++)
    {
        temp = 0;
        for (int m = 0; m < rozmer; m++)
        {
            temp = temp + matice1[j][m] * tempmat[k][m];
        }
        matice3[j][k] = temp;
    }
}
timer.stop();

因此,您可以看到缓存局部性如何大大提高代码的性能。现在真正的dgemm实现将它用于非常广泛的层次:它们对由TLB的大小定义的矩阵块进行乘法(转换后备缓冲区,长话短说:可以有效缓存的内容),以便它们向处理器准确地传输它可以处理的数据量。另一方面是矢量化,它们使用处理器的矢量化指令来获得最佳指令吞吐量,而您无法通过跨平台C ++代码实现这一点。

最后,由于Strassen或Coppersmith-Winograd算法而声称它们是错误的,因为上面提到的硬件考虑因素,这两种算法在实践中都不可实现。

答案 1 :(得分:81)

以下是在使用Tesla C2070的机器上使用MATLAB R2011a + Parallel Computing Toolbox的结果:

>> A = rand(1024); gA = gpuArray(A);
% warm up by executing the operations a couple of times, and then:
>> tic, C = A * A; toc
Elapsed time is 0.075396 seconds.
>> tic, gC = gA * gA; toc
Elapsed time is 0.008621 seconds.

MATLAB使用高度优化的库进行矩阵乘法,这就是普通MATLAB矩阵乘法如此之快的原因。 gpuArray版本使用MAGMA

在具有Tesla K20c的计算机上使用R2014a 进行

更新,以及新的timeitgputimeit功能:

>> A = rand(1024); gA = gpuArray(A);
>> timeit(@()A*A)
ans =
    0.0324
>> gputimeit(@()gA*gA)
ans =
    0.0022
在具有16个物理内核和Tesla V100的WIN64计算机上使用R2018b
进行

更新:

>> timeit(@()A*A)
ans =
    0.0229
>> gputimeit(@()gA*gA)
ans =
   4.8019e-04

答案 2 :(得分:39)

This is why。 MATLAB不会像在C ++代码中那样循环遍历每个元素,从而不执行简单的矩阵乘法。

当然我假设您刚刚使用C=A*B而不是自己编写乘法函数。

答案 3 :(得分:19)

Matlab在不久前收录了LAPACK,所以我假设他们的矩阵乘法至少使用了那么快的东西。 LAPACK源代码和文档随时可用。

你也可以看看Goto和Van De Geijn的论文“高性能矩阵的剖析” 乘法“在http://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.140.1785&rep=rep1&type=pdf

答案 4 :(得分:9)

在进行矩阵乘法时,使用天真乘法方法,该方法需要O(n^3)的时间。

存在矩阵乘法算法,它采用O(n^2.4)。这意味着在n=2000您的算法需要的计算量是最佳算法的约100倍 您应该检查维基百科页面中的矩阵乘法,以获得有关实现它的有效方法的更多信息。

答案 5 :(得分:9)

答案是LAPACKBLAS库使得MATLAB在矩阵运算时的速度非常快,而不是MATLAB人员的任何专有代码。

使用C ++代码中的LAPACK和/或BLAS库进行矩阵运算,您应该获得与MATLAB类似的性能。这些图书馆应该可以在任何现代系统上免费获得,而且这些图书馆在学术界已有数十年的历请注意,有多个实现,包括一些封闭的源,如Intel MKL

讨论BLAS如何获得高性能is available here.

顺便说一下,直接从c调用LAPACK库是一种严重的痛苦(但值得)。您需要非常准确地阅读文档。

答案 6 :(得分:6)

根据您的Matlab版本,我相信它可能已经在使用您的GPU了。

另一件事; Matlab会跟踪矩阵的许多属性;它的对角线,hermetian等等,并专门研究其基于此的算法。也许它的专业化基于您传递的零矩阵,或类似的东西?也许它正在缓存重复的函数调用,这会扰乱你的时间?也许它优化了重复使用的矩阵产品?

为了防止发生这种情况,请使用随机数字矩阵,并确保通过将结果打印到屏幕或磁盘或其他某些部分来强制执行。

答案 7 :(得分:2)

“为什么matlab在执行xxx时比其他程序更快”的一般答案是matlab有很多内置的优化函数。

使用的其他程序通常没有这些功能,所以人们应用自己的创意解决方案,这比专业优化的代码慢得多。

这可以用两种方式解释:

1)常见/理论方式:Matlab并没有明显更快,你只是做了基准错误

2)现实的方法:对于这个东西,Matlab在实践中更快,因为作为c ++的语言太容易以无效的方式使用。

答案 8 :(得分:2)

鲜明的对比不仅仅是由于Matlab的惊人优化(正如许多其他答案所讨论的那样),而且还因为你将矩阵表示为一个对象。

好像你把矩阵列成了一个列表?列表列表包含指向列表的指针,然后列表包含矩阵元素。包含列表的位置是任意分配的。在循环第一个索引(行号?)时,内存访问的时间非常重要。相比之下,为什么不尝试使用以下方法将矩阵实现为单个列表/向量?

#include <vector>

struct matrix {
    matrix(int x, int y) : n_row(x), n_col(y), M(x * y) {}
    int n_row;
    int n_col;
    std::vector<double> M;
    double &operator()(int i, int j);
};

并且

double &matrix::operator()(int i, int j) {
    return M[n_col * i + j];
}

应使用相同的乘法算法,以使翻牌的数量相同。 (对于大小为n的平方矩阵,n ^ 3)

我要求你计时,以便结果与之前的(在同一台机器上)相当。通过比较,您将准确显示内存访问时间的重要性!

答案 9 :(得分:2)

它在C ++中很慢,因为你没有使用多线程。基本上,如果A = BC,它们都是矩阵,A的第一行可以独立于第二行计算,等等。如果A,B和C都是n乘n矩阵,你可以加速乘法因子为n ^ 2,

a_ {i,j} = sum_ {k} b_ {i,k} c_ {k,j}

如果你使用,例如,Eigen [http://eigen.tuxfamily.org/dox/GettingStarted.html],多线程是内置的,线程数是可调的。

答案 10 :(得分:2)

因为 MATLAB 是最初为数字线性代数(矩阵操纵)开发的一种编程语言,它具有专门为矩阵乘法开发的库。并且 now MATLAB还可以为此另外使用 GPUs (Graphics processing unit)

如果我们查看您的计算结果:

             1024x1024   2048x2048   4096x4096
             ---------   ---------   ---------
CUDA C (ms)      43.11      391.05     3407.99
C++ (ms)       6137.10    64369.29   551390.93
C# (ms)       10509.00   300684.00  2527250.00
Java (ms)      9149.90    92562.28   838357.94
MATLAB (ms)      75.01      423.10     3133.90

然后我们可以看到,不仅MATLAB在矩阵乘法方面是如此之快: CUDA C (NVIDIA的编程语言)比MATLAB具有更好的结果。 CUDA C还具有专门为矩阵乘法开发的库,并使用GPU。

MATLAB的短历史记录

  

新墨西哥大学计算机科学系主任克莱夫·莫勒(Cleve Moler)在1970年代后期开始开发MATLAB。他设计了它,使学生可以访问 LINPACK (用于执行数值线性代数的软件库)和 EISPACK (用于线性代数的数值计算),而无需学习Fortran。它很快传播到其他大学,并在应用数学界引起了广泛的关注。工程师Jack Little在1983年Moler对斯坦福大学的访问中接触了它。认识到它的商业潜力,他与Moler和Steve Bangert一起加入了。他们用C语言重写了MATLAB,并于1984年成立了MathWorks,以继续其发展。这些重写的库称为JACKPAC。 2000年,对MATLAB进行了重写,以使用一组新的用于矩阵处理的库LAPACK(这是用于数字线性代数的标准软件库)。

     

Source

什么是CUDA C

CUDA C还使用专门为矩阵乘法而开发的库,例如 OpenGL (开放图形库)。它还使用GPU和Direct3D(在MS Windows上)。

  

CUDA platform 旨在与C,C ++和Fortran等编程语言一起使用。与以前需要高级功能的 Direct3D OpenGL 之类的API相比,这种可访问性使并行编程专家更容易使用GPU资源。图形编程技巧。另外,CUDA支持编程框架,例如 OpenACC OpenCL

     

enter image description here

     

CUDA处理流程示例:

     
      
  1. 将数据从主内存复制到GPU内存
  2.   
  3. CPU启动GPU计算内核
  4.   
  5. GPU的CUDA内核并行执行内核
  6.   
  7. 将结果数据从GPU内存复制到主内存
  8.   

比较CPU和GPU的执行速度

  

我们运行了一个基准测试,其中测量了在Intel Xeon Processor X5650上使用NVIDIA Tesla C2050 GPU执行64、128、512、1024和2048网格大小的50个时间步所花费的时间

     

enter image description here

     

对于2048的网格大小,该算法显示计算时间减少了7.5倍,从CPU上的一分钟减少到GPU上的不到10秒。日志比例图显示,对于较小的网格大小,CPU实际上更快。但是,随着技术的发展和成熟,GPU解决方案越来越能够处理较小的问题,我们希望这种趋势还将继续。

     

Source

摘自《 CUDA C编程指南》的简介:

  

受市场对实时高清3D图形的不满足需求的驱使,可编程图形处理器单元(GPU)已发展成为高度并行的多线程,多核处理器,具有强大的计算能力和非常高的内存带宽,如{ {1}}和Figure 1

     

图1。 CPU和GPU的每秒浮点操作

     

enter image description here

     

图2 。 CPU和GPU的内存带宽

     

enter image description here

     

CPU和GPU之间的浮点功能差异背后的原因是,GPU专用于计算密集型,高度并行的计算-正是图形渲染所针对的-因此设计为可以使用更多的晶体管Figure 2示意性地说明了数据处理而不是数据缓存和流控制。

     

图3 。 GPU将更多晶体管用于数据处理

     

enter image description here

     

更具体地说,GPU特别适合解决可以表示为数据并行计算的问题-在许多数据元素上并行执行同一程序-具有很高的算术强度-算术运算与内存的比率操作。由于对每个数据元素执行相同的程序,因此对复杂的流控制的要求较低,并且由于它对许多数据元素执行并且具有很高的算术强度,因此可以通过计算而不是大数据缓存来隐藏内存访问延迟。

     

数据并行处理将数据元素映射到并行处理线程。许多处理大型数据集的应用程序可以使用数据并行编程模型来加快计算速度。在3D渲染中,大量像素和顶点被映射到并行线程。同样,图像和媒体处理应用程序(例如渲染图像的后处理,视频编码和解码,图像缩放,立体视觉和模式识别)可以将图像块和像素映射到并行处理线程。实际上,从通用信号处理或物理模拟到计算金融或计算生物学,数据并行处理可加速图像渲染和处理领域之外的许多算法。

     

Source

高级阅读


一些有趣的facs

  

我写的C ++矩阵乘法与Matlab一样快,但是要多加注意。 (在Matlab为此使用GPU之前)。

     

来自 this answer 的引用。