这类问题反复出现,应该比 Stack Overflow 上一次“MATLAB 使用高度优化的库”或“MATLAB 使用 MKL”更清楚地回答。
历史:
矩阵乘法(连同矩阵向量、向量向量乘法和许多矩阵分解)是(是)线性代数中最重要的问题。从早期开始,工程师就一直在用计算机解决这些问题。
我不是历史专家,但显然在那时,每个人都只是用简单的循环重写了他的 FORTRAN 版本。随后出现了一些标准化,并确定了大多数线性代数问题需要解决的“内核”(基本例程)。这些基本操作随后在称为基本线性代数子程序 (BLAS) 的规范中标准化。然后,工程师可以在他们的代码中调用这些标准的、经过良好测试的 BLAS 例程,从而使他们的工作变得更加容易。
布拉斯:
BLAS从level 1(定义标量向量和向量向量运算的第一个版本)到level 2(向量矩阵运算)再到level 3(矩阵矩阵运算),并提供越来越多的“内核”,因此更加标准化以及更多基本的线性代数运算。最初的 FORTRAN 77 实现仍然可以在Netlib 的网站上找到。
实现更好的性能:
所以多年来(特别是在 BLAS 1 级和 2 级版本之间:80 年代初),随着向量操作和缓存层次结构的出现,硬件发生了变化。这些演变使得大幅提高 BLAS 子程序的性能成为可能。然后,不同的供应商推出了他们的 BLAS 例程实现,这些例程越来越高效。
我不知道所有的历史实现(那时我还不是出生或孩子),但在 2000 年代初期出现了两个最著名的实现:英特尔 MKL 和 GotoBLAS。您的 Matlab 使用英特尔 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 算法是错误的,由于上述硬件考虑,这两种算法在实践中都无法实现。