与“ MATLAB使用高度优化的库”或“ MATLAB使用MKL”一次在Stack Overflow上相比,此类问题还是很常见。
历史:
矩阵乘法(连同矩阵向量,向量向量乘法以及许多矩阵分解)是线性代数中最重要的问题。从早期开始,工程师就一直在使用计算机解决这些问题。
我不是历史专家,但是很明显,那时,每个人都只是用简单的循环重写了他的FORTRAN版本。随之而来的是一些标准化,其中包括“内核”(基本例程)的识别,这是解决大多数线性代数问题所需要的。然后,在称为“基本线性代数子程序(BLAS)”的规范中将这些基本操作标准化。然后,工程师可以在代码中调用这些经过良好测试的标准BLAS例程,从而使工作变得更加轻松。
BLAS:
BLAS从1级(定义标量向量和矢量向量运算的第一个版本)发展到2级(向量矩阵运算)到3级(矩阵矩阵运算),并提供了越来越多的“内核”,因此更加标准化以及更多基本的线性代数运算。最初的FORTRAN 77实现仍可在Netlib的网站上找到。
为了获得更好的性能:
因此,多年来(尤其是在BLAS 1级和2级发行之间:80年代初),随着矢量操作和缓存层次结构的出现,硬件发生了变化。这些改进使得有可能大大提高BLAS子例程的性能。然后,不同的供应商带来了越来越高效的BLAS例程实现。
我不知道所有的历史实现(当时我还不是出生或孩子),但是其中两个最著名的实现出现在2000年代初:Intel MKL和GotoBLAS。您的Matlab使用Intel MKL,它是一个非常好的,经过优化的BLAS,它可以解释您所看到的出色性能。
矩阵乘法的技术细节:
那么,为什么Matlab(MKL)这么快dgemm
(双精度通用矩阵-矩阵乘法)呢?简而言之:因为它使用向量化和良好的数据缓存。用更复杂的术语来说:请参阅乔纳森·摩尔(Jonathan Moore)提供的文章。
基本上,当您使用提供的C ++代码执行乘法时,您根本就不会缓存。由于我怀疑您创建了一个指向行数组的指针数组,因此您在内部循环中对“ matice2”的第k列的访问matice2[m][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算法是错误的,由于上述硬件考虑,这两种算法在实践中均无法实现。