作为一项实验,我实施了Strassen矩阵乘法算法,以确定是否真正为大n提供了更快的代码。
https://github.com/wcochran/strassen_multiplier/blob/master/mm.c
令我惊讶的是,对于大n来说,方式更快。例如,n = 1024的情况 使用传统方法花了17.20秒,而它只用了1.13秒 使用Strassen方法(2x2.66 GHz Xeon)。什么 - 加速15倍!?它应该只是略快一点。事实上,即使是小型的32x32矩阵,它似乎也同样好!?
我能够解释这么多加速的唯一方法是我的算法更加缓存友好 - 即,它专注于小块矩阵,因此数据更加本地化。也许我应该尽可能地完成所有矩阵运算。
关于为什么这么快的任何其他理论?
答案 0 :(得分:3)
Strassen的递归性质具有更好的记忆局部性, 这可能是图片的一部分。一个递归的常规 矩阵乘法也许是一个合理的事情 比较。
答案 1 :(得分:1)
第一个问题是“结果是否正确?”。如果是这样,那么“传统”方法可能不是一个好的实现方式。
传统方法不是使用3个嵌套的FOR循环来按照您在数学课中学习的顺序扫描输入。一个简单的改进是将矩阵转置到右侧,使其位于内存中,列是连贯的而不是行。修改乘法循环以使用此备用布局,它将在大矩阵上运行得更快。
标准矩阵库实现了更多缓存友好的方法,这些方法考虑了数据缓存的大小。
您也可以实现标准矩阵乘积的递归版本(细分为2x2矩阵的矩阵的一半)。这将提供更接近最佳缓存性能的东西,strassen从递归中获得。
所以要么你做错了,要么你的传统代码没有优化。
答案 2 :(得分:0)
传统乘法中的循环次序是多少?如果你有
for (int i = 0; i < new_height; ++i)
{
for (int j = 0; j < new_width; ++j)
{
double sum = 0.0;
for (int k = 0; k < common; ++k)
{
sum += lhs[i * common + k] * rhs[k * new_width + j];
}
product[i * new_width + j] = sum;
}
}
然后你对缓存不是很好,因为你以非连续的方式访问右侧矩阵。重新排序后
for (int i = 0; i < new_height; ++i)
{
for (int k = 0; k < common; ++k)
{
double const fixed = lhs[i * common + k];
for (int j = 0; j < new_width; ++j)
{
product[i * new_width + j] += fixed * rhs[k * new_width + j];
}
}
}
在最内层循环中访问两个矩阵是连续的,一个甚至是固定的。一个好的编译器可能会自动执行此操作,但我选择明确地将其拉出来进行演示。
您没有指定语言,但对于C ++,高级编译器甚至会在某些配置中识别出不友好的循环顺序并重新排序。