如何在时间复杂度方面优化cpp中的矩阵乘法?
how to optimize matrix multiplication in cpp in terms of time complexity?
给定任意 2 个矩阵 a 和 b(它们没有特殊属性),我们是否有比这更好的计算乘法的方法:?
for(i=0; i<r1; ++i)
for(j=0; j<c2; ++j)
for(k=0; k<c1; ++k)
{
mult[i][j]+=a[i][k]*b[k][j];
}
如果你好奇它们在理论上是否存在,那么是的。例如,Strassen 算法(参见 https://en.wikipedia.org/wiki/Strassen_algorithm). And it's not even the fastest we know. As far as I'm concerned the best for now is Coppersmith–Winograd algorithm (see https://en.wikipedia.org/wiki/Coppersmith%E2%80%93Winograd_algorithm),它类似于 O(n^{2.37})
(Strassen 的时间复杂度类似于 O(n^{2.8})
.
但在实践中,它们比你写的更难实现,而且它们在 O()
下隐藏了相当大的时间常数,所以你写的 O(n^3)
算法在低值时甚至更好n
并且更容易实施。
还有一个 Strassen 的假设,它声称对于每个 eps > 0
都有一个算法将两个矩阵相乘,时间复杂度为 O(n^{2 + eps})
。但您可能已经注意到,目前这只是一个假设。
作为一个非常简单的解决方案,您可以在乘法之前转置第二个矩阵,这样您的代码将获得更少的处理器缓存未命中。复杂度将相同,但它可能会稍微提高时间常数。
这些是这个世界上许多聪明的灵魂在你面前解决的问题。不要折磨自己,使用 BLAS ?GEMM.
您可以通过将乘法分配给它们来使用多个线程。因此,将第一个矩阵的第一个维度或最后一个矩阵的最后一个维度的 lines/columns 分成与处理器中的内核数相等的任务。如果这些不能被均匀分割,一些核心将不得不做一个额外的周期。但是无论如何,这个想法是将乘法运算给更多的核心并进行除法,例如第一个矩阵分为 4 个部分(我有 4 个核心),用 4 个任务进行乘法运算,然后重新组装(这不是必需的,因为核心可能处理相同的数据)。
这是一个很好的问题,值得比 "use a library" 更完整的答案。
当然,如果你想做好,你可能不应该尝试自己写。但是如果这个问题是关于学习如何更快地进行矩阵乘法,这里有一个完整的答案。
实际上,您显示的代码对内存的写入过多。如果内层循环在一个标量变量中加上点积,那么只写在最后,代码会更快。大多数编译器不够聪明,无法理解这一点。
双点=0;
对于(k=0;k
这也提高了多核性能,因为如果您使用多核,它们必须共享内存带宽。
如果您使用的是行数组,请将您的表示切换为单个内存块。
正如上面有人提到的,你可以做一个转置,这样矩阵遍历都是按顺序进行的。内存旨在按顺序有效地读取,但是您的 b[k][j] 会跳来跳去,因此随着大小变大,这通常会快 3 倍(大约 1000x1000,初始转置的成本可以忽略不计) .
当矩阵足够大时,Strassen 和 Coppersmith-Winograd 是更快的乘法方法,从根本上改变了规则,但他们通过巧妙地重新排列项来实现相同的理论结果,但成本更低复杂性限制。实际上,他们会更改答案,因为舍入误差不同,并且对于大型矩阵,这些算法产生的答案可能比蛮力乘法差得多。
如果你有一台真正的并行计算机,你可以将矩阵复制到多个 CPU,让它们并行处理答案。
您可以将代码放到您的视频卡上,并使用具有更多内存带宽的更多并行 CPU。这可能是在您的计算机上获得真正加速的最有效方法(假设您有显卡)。参见 CUDA 或 Vulkan。
根本问题是多核对矩阵乘法没有多大帮助,因为您受到内存带宽的限制。这就是为什么在视频卡上这样做非常好,因为那里的带宽要高得多。
给定任意 2 个矩阵 a 和 b(它们没有特殊属性),我们是否有比这更好的计算乘法的方法:?
for(i=0; i<r1; ++i)
for(j=0; j<c2; ++j)
for(k=0; k<c1; ++k)
{
mult[i][j]+=a[i][k]*b[k][j];
}
如果你好奇它们在理论上是否存在,那么是的。例如,Strassen 算法(参见 https://en.wikipedia.org/wiki/Strassen_algorithm). And it's not even the fastest we know. As far as I'm concerned the best for now is Coppersmith–Winograd algorithm (see https://en.wikipedia.org/wiki/Coppersmith%E2%80%93Winograd_algorithm),它类似于 O(n^{2.37})
(Strassen 的时间复杂度类似于 O(n^{2.8})
.
但在实践中,它们比你写的更难实现,而且它们在 O()
下隐藏了相当大的时间常数,所以你写的 O(n^3)
算法在低值时甚至更好n
并且更容易实施。
还有一个 Strassen 的假设,它声称对于每个 eps > 0
都有一个算法将两个矩阵相乘,时间复杂度为 O(n^{2 + eps})
。但您可能已经注意到,目前这只是一个假设。
作为一个非常简单的解决方案,您可以在乘法之前转置第二个矩阵,这样您的代码将获得更少的处理器缓存未命中。复杂度将相同,但它可能会稍微提高时间常数。
这些是这个世界上许多聪明的灵魂在你面前解决的问题。不要折磨自己,使用 BLAS ?GEMM.
您可以通过将乘法分配给它们来使用多个线程。因此,将第一个矩阵的第一个维度或最后一个矩阵的最后一个维度的 lines/columns 分成与处理器中的内核数相等的任务。如果这些不能被均匀分割,一些核心将不得不做一个额外的周期。但是无论如何,这个想法是将乘法运算给更多的核心并进行除法,例如第一个矩阵分为 4 个部分(我有 4 个核心),用 4 个任务进行乘法运算,然后重新组装(这不是必需的,因为核心可能处理相同的数据)。
这是一个很好的问题,值得比 "use a library" 更完整的答案。
当然,如果你想做好,你可能不应该尝试自己写。但是如果这个问题是关于学习如何更快地进行矩阵乘法,这里有一个完整的答案。
实际上,您显示的代码对内存的写入过多。如果内层循环在一个标量变量中加上点积,那么只写在最后,代码会更快。大多数编译器不够聪明,无法理解这一点。
双点=0; 对于(k=0;k
这也提高了多核性能,因为如果您使用多核,它们必须共享内存带宽。 如果您使用的是行数组,请将您的表示切换为单个内存块。
正如上面有人提到的,你可以做一个转置,这样矩阵遍历都是按顺序进行的。内存旨在按顺序有效地读取,但是您的 b[k][j] 会跳来跳去,因此随着大小变大,这通常会快 3 倍(大约 1000x1000,初始转置的成本可以忽略不计) .
当矩阵足够大时,Strassen 和 Coppersmith-Winograd 是更快的乘法方法,从根本上改变了规则,但他们通过巧妙地重新排列项来实现相同的理论结果,但成本更低复杂性限制。实际上,他们会更改答案,因为舍入误差不同,并且对于大型矩阵,这些算法产生的答案可能比蛮力乘法差得多。
如果你有一台真正的并行计算机,你可以将矩阵复制到多个 CPU,让它们并行处理答案。
您可以将代码放到您的视频卡上,并使用具有更多内存带宽的更多并行 CPU。这可能是在您的计算机上获得真正加速的最有效方法(假设您有显卡)。参见 CUDA 或 Vulkan。
根本问题是多核对矩阵乘法没有多大帮助,因为您受到内存带宽的限制。这就是为什么在视频卡上这样做非常好,因为那里的带宽要高得多。