我在C++中写了两个矩阵乘法程序:正则MM(源)和Strassen的MM(源),它们都在大小为2 ^ ^ k×2 ^ k的方阵上(换句话说,偶数大小的正方形矩阵)。
结果很糟糕。对于1024 x 1024矩阵,正则mm取46.381 sec,而strassen的mm取1484.303 sec(25 minutes!!!!!!)
我试图使代码尽可能简单。在Web上发现的其他Strassen的mm示例与我的代码没有太大的不同。斯特拉森代码的一个问题是显而易见的——我没有临界点,它切换到常规的mm。
我的Strassen的mm代码还有什么问题????
谢谢!
直接链接到源网址:http://pastebin.com/hqhtfpq9http://pastebin.com/usrq5tuy
编辑一。首先,有很多好的建议。感谢您抽出时间和分享知识。
我实现了更改(保留了所有代码),添加了截止点。毫米的2048x2048矩阵,与切断512已经给出了良好的结果。常规mm:191.49s斯特拉森毫米:112.179秒显著改善。在使用Intel Centrino处理器的史前联想X61 TableTPC上使用Visual Studio 2012获得了结果。我会做更多的检查(以确保得到正确的结果),并发布结果。
- @卢奇昂:哦,这很微妙。很可能也是问题的一个重要部分。可能比我确定的问题更大。
- @LuchiangrigoreWoah,你是怎么得出这个结论的?在这个问题上,没有一个地方可以说只有两种力量的情况下才会变慢。如果有什么区别的话,我敢打赌1024根本不足以克服开销。
- @神秘的,我认为其中一个算法是缓存遗忘,因为维度固定为2^k。可能是错误的。
- @神秘:代码中所有可能的大小都是2的幂。
- @Luchiangrigore:一种算法非常简单(而不是缓存遗忘)。另一个也不是缓存遗忘,尽管它应该快得多。
- @无所不知,我会测试不是2次幂的尺寸-如果问题消失了,那就是原因。
- @Luchiangrigore:不幸的是,我相信Strassen的算法是一种递归的分而治之的算法。我怀疑它可以为2的非权力工作。但我也怀疑操作系统的实现并没有。
- @无所不知,我特意为2的幂设计代码,否则部分代码需要重写,因为设计后的矩阵将不均匀,需要填充(零行)。
- @Luchiangrigore你是对的!!
- @好吧,那么,原因就是这里解释的
- @很明显是神秘的
- 我认为两个超级对齐的能力会比Strassen算法更伤害正常算法,因为后者是缓存遗忘的。也许我错过了什么。
- @newprint我刚刚意识到您已经知道递归到1的问题。所以我扩大了我的答案。基本上,性能的下降会使其他东西相形见绌。因此,除非您有一个稍微优化的实现,否则您无法得出任何结论。
One issue with Strassen's code is obvious - I don't have cutoff point,
that switches to regular MM.
公平地说,递归到1点是(如果不是整个)问题的主要部分。尝试在不解决这一问题的情况下猜测其他性能瓶颈几乎是没有意义的,因为它带来了巨大的性能冲击。(换句话说,你把苹果比作橙子。)
正如注释中所讨论的,缓存对齐可能会产生影响,但不会达到这个比例。此外,缓存对齐可能比Strassen算法更损害常规算法,因为后者是缓存遗忘的。
1 2 3 4 5 6 7
| void strassen(int **a, int **b, int **c, int tam) {
// trivial case: when the matrix is 1 X 1:
if (tam == 1) {
c[0][0] = a[0][0] * b[0][0];
return;
} |
那太小了。虽然strassen算法的复杂性较小,但它有一个更大的big-o常量。首先,函数调用开销一直降到1个元素。
这类似于使用合并或快速排序并一直递归到一个元素。为了提高效率,需要在大小变小时停止递归,然后返回到经典算法。
在快速/合并排序中,您将返回到低开销的O(n^2)插入或选择排序。这里你可以回到正常的O(n^3)矩阵乘法。
返回经典算法的阈值应该是一个可调的阈值,根据硬件和编译器优化代码的能力可能会有所不同。
对于像strassen乘法这样的东西,其优势仅仅是O(2.8074)比经典的O(n^3),如果这个阈值非常高,不要感到惊讶。(成千上万的元素?)
在某些应用中,可以有许多算法,每一种算法的复杂度都会降低,但大O值却会增加。结果是,多个算法在不同的大小下会变得最优。
大整数乘法就是一个臭名昭著的例子:
- 小学乘法:o(n^2)最适合<~100位数字*
- Karatsuba乘法:0(n^1.585)比上面快大约100位*
- toom cook 3路:o(n^1.465)比karatusuba快约3000位*
- 浮点fft:o(>n log(n))比karatusuba/toom-3快约700位*
- SCH?nhage–Strassen算法(SSA):o(n log(n)loglog(n))比fft快约10亿位*
- 固定宽度数理论转换:O(n对数(n)比SSA快几十亿位?*
*注意,这些示例阈值是近似值,可以大幅度变化-通常超过10的系数。
- 这就是我喜欢StackOverflow的原因之一。通过一个问题,我看到了现实世界中的一些例子,其中可以产生性能问题的细微影响被放大并以一种明显的方式展示出来。然后,当然,这个答案很可能是导致算法更快变慢的原因。
- 我测试过,对于幼稚的算法,我指出来的问题是很重要的。但是它应该对这两种算法都有显著的影响,所以这可能不是所谓更快的算法性能不佳的原因。
- @无所不知是的,我希望两个联盟惩罚的力量不超过3倍。因为这四个主要的例子都是这样的,所以只有3倍的性能。这里的操作有30倍的性能差异。
- 你的底线实在太高了。
- @tmyklebu哪些截止?
- @神秘的-哦,不,我的问题比较简单。我感到恼火的是,每一行都被单独分配,这会杀死引用的位置,并对每个元素访问引入间接寻址。
- @无所不能的等待,为什么缓存一致性和它有任何关系?这不是线程。
- @神秘的:哎呀,用错了词。参考位置。唉,天已经晚了,我的思想也模糊了。
- @无所不能啊…在这种情况下,牺牲位置来破坏对齐实际上提高了性能。
- @神秘:是的,我在想。尽管间接性也是一个问题。因此,还不清楚所有的竞争效应将如何产生。我的经验测试表明,对于简单的算法,连续地分配数组可以将性能提高50%,而这种算法对缓存的遗忘程度最小。
- @神秘的:只是吹毛求疵,真的。我的相当老的GMP装置有31个肢体(约600位)的Karatsuba乘法的截止点,TOOM3约2000位,FFT约150000位。我认为他们在最近的版本中大大改进了FFT乘法。快速傅立叶变换乘法的阈值不在数十亿。浮点FFT的一个问题是它实际上不适用于非平凡的操作数大小。(似乎是这样,但事实并非如此。)
- @米克勒布啊。所以我给了10倍。这些数字大致就是y-cruncher中使用的值。GMP不使用浮点FFT,因为它们害怕舍入。所以当你考虑浮点FFT时,它会将SSA(GMP使用的)的阈值推高到数十亿。超过100000位左右,Y-Cruncher使用了一整套未发布的算法。所以我不知道SSA和NTT的确切阈值,除了"几十亿"之外——因为我从未测试过它。
所以,可能还有更多的问题需要解决,但第一个问题是您使用的是指向数组的指针数组。由于您使用的数组大小是2的幂,这对于连续分配元素和使用整数除法将长数组的数字折叠成行来说是一个特别大的性能冲击。
不管怎样,这是我对一个问题的第一个猜测。正如我所说,可能会有更多的答案,当我发现它们时,我会补充这个答案。
编辑:这可能只会对问题造成少量影响。这个问题很可能是Luchian Grigore提到的涉及缓存线争用问题,其权力为2。
我验证了我的关注对于幼稚的算法是有效的。如果数组是连续的,那么简单算法的时间减少了近50%。这里是使用巴斯丁的代码(使用C++ 11依赖的平方矩阵类)。
- 谢谢你的帮助!我早上会看一下你的代码。
- @newprint:我犯了几个小错误,这些错误对代码没有影响,但使SquareMatrix类不适合一般使用。我会修好的。
- 这里的simpe version-void madd(int n,int xpitch,const double x[],int ypitch,const double y[],int spitch,double s[])for(int i=0;i
- ezekiel.couver.wsu.edu/~cs330/讲座/线性代数/mm/&zwnj;&8203;&hellip;
- @Newprint:我不喜欢这个版本,因为你必须记住每次矩阵访问都要使用乘法。但它是非常好的C-ISH,并且根本不使用C++特性。:-)我的版本(及其内联函数)允许编译器做出一些有趣的假设,并进行一些非常好的优化,同时允许实际的乘法算法仍然清晰地使用多维数组访问。