Matrix 在Mathematica中,构造大块矩阵最有效的方法是什么?
受Mike Bantegui关于构造一个定义为递归关系的矩阵的启发,我想知道是否有关于在最短计算时间内建立大型块矩阵的一般指导。根据我的经验,构建块然后将它们组合在一起可能效率很低(因此我的答案实际上比Mike的原始代码慢)Matrix 在Mathematica中,构造大块矩阵最有效的方法是什么?,matrix,wolfram-mathematica,Matrix,Wolfram Mathematica,受Mike Bantegui关于构造一个定义为递归关系的矩阵的启发,我想知道是否有关于在最短计算时间内建立大型块矩阵的一般指导。根据我的经验,构建块然后将它们组合在一起可能效率很低(因此我的答案实际上比Mike的原始代码慢)Join和可能的ArrayFlatten的效率可能比它们低 显然,如果矩阵是稀疏的,则可以使用SparseMatrix构造,但有时要构造的块矩阵不是稀疏的 针对此类问题的最佳做法是什么?我假设矩阵的元素是数字。下面显示的代码在这里可用:。只需将其复制并粘贴到笔记本中进行测试。
Join
和可能的ArrayFlatten
的效率可能比它们低
显然,如果矩阵是稀疏的,则可以使用SparseMatrix
构造,但有时要构造的块矩阵不是稀疏的
针对此类问题的最佳做法是什么?我假设矩阵的元素是数字。下面显示的代码在这里可用:。只需将其复制并粘贴到笔记本中进行测试。 实际上,我尝试了几种计算矩阵的函数方法,因为 我认为函数方法(在Mathematica中通常是惯用的)更有效 例如,我有一个由两个列表组成的矩阵:
In: L = 1200;
e = Table[..., {2L}];
f = Table[..., {2L}];
h = Table[0, {2L}, {2L}];
Do[h[[i, i]] = e[[i]], {i, 1, L}];
Do[h[[i, i]] = e[[i-L]], {i, L+1, 2L}];
Do[h[[i, j]] = f[[i]]f[[j-L]], {i, 1, L}, {j, L+1, 2L}];
Do[h[[i, j]] = h[[j, i]], {i, 1, 2 L}, {j, 1, i}];
我的第一步是给一切计时
In: h = Table[0, {2 L}, {2 L}];
AbsoluteTiming[Do[h[[i, i]] = e[[i]], {i, 1, L}];]
AbsoluteTiming[Do[h[[i, i]] = e[[i - L]], {i, L + 1, 2 L}];]
AbsoluteTiming[
Do[h[[i, j]] = f[[i]] f[[j - L]], {i, 1, L}, {j, L + 1, 2 L}];]
AbsoluteTiming[Do[h[[i, j]] = h[[j, i]], {i, 1, 2 L}, {j, 1, i}];]
Out: {0.0020001, Null}
{0.0030002, Null}
{5.0012861, Null}
{4.0622324, Null}
DiagonalMatrix[…]
比do循环慢,所以我决定在最后一步中只使用do
循环。如您所见,在这种情况下,使用Outer[Times,f,f]
要快得多
然后,我使用Outer
为矩阵右上角和左下角的块编写了等效代码,并使用DiagonalMatrix
为对角线编写了等效代码:
AbsoluteTiming[h1 = ArrayPad[Outer[Times, f, f], {{0, L}, {L, 0}}];]
AbsoluteTiming[h1 += Transpose[h1];]
AbsoluteTiming[h1 += DiagonalMatrix[Join[e, e]];]
Out: {0.9960570, Null}
{0.3770216, Null}
{0.0160009, Null}
对角矩阵实际上较慢。我可以用Do
循环来代替它,但我保留了它,因为它看起来更干净
对于naiveDo
循环,当前计数为9.06秒,对于使用Outer
和DiagonalMatrix
的下一个版本,当前计数为1.389秒。大约6.5倍的加速,还不错
听起来快多了,不是吗?现在让我们尝试使用Compile
In: cf = Compile[{{L, _Integer}, {e, _Real, 1}, {f, _Real, 1}},
Module[{h},
h = Table[0.0, {2 L}, {2 L}];
Do[h[[i, i]] = e[[i]], {i, 1, L}];
Do[h[[i, i]] = e[[i - L]], {i, L + 1, 2 L}];
Do[h[[i, j]] = f[[i]] f[[j - L]], {i, 1, L}, {j, L + 1, 2 L}];
Do[h[[i, j]] = h[[j, i]], {i, 1, 2 L}, {j, 1, i}];
h]];
AbsoluteTiming[cf[L, e, f];]
Out: {0.3940225, Null}
现在它比我的上一个版本快3.56倍,比第一个版本快23.23倍。下一版本:
In: cf = Compile[{{L, _Integer}, {e, _Real, 1}, {f, _Real, 1}},
Module[{h},
h = Table[0.0, {2 L}, {2 L}];
Do[h[[i, i]] = e[[i]], {i, 1, L}];
Do[h[[i, i]] = e[[i - L]], {i, L + 1, 2 L}];
Do[h[[i, j]] = f[[i]] f[[j - L]], {i, 1, L}, {j, L + 1, 2 L}];
Do[h[[i, j]] = h[[j, i]], {i, 1, 2 L}, {j, 1, i}];
h], CompilationTarget->"C", RuntimeOptions->"Speed"];
AbsoluteTiming[cf[L, e, f];]
Out: {0.1370079, Null}
大部分速度来自于编译目标-->“C”
。在这里,我比最快的版本有2.84的加速比,比第一个版本有66.13倍的加速比。但我所做的只是编译它
这是一个非常简单的例子。但这是我用来解决凝聚态物理问题的真实代码。因此,不要将其视为一个“玩具示例”
另一个我们可以使用的技术的例子怎么样?我要建立一个相对简单的矩阵。我有一个矩阵,它从一开始到任意点都由一个矩阵组成。天真的方式可能看起来像这样:
In: k = L;
AbsoluteTiming[p = Table[If[i == j && j <= k, 1, 0], {i, 2L}, {j, 2L}];]
Out: {5.5393168, Null}
这实际上不适用于k=0,但如果需要,可以在特殊情况下使用。此外,根据k的大小,这可能更快或更慢。但它总是比表[…]版本快
您甚至可以使用SparseArray编写此代码:
In: AbsoluteTiming[SparseArray[{i_, i_} /; i <= k -> 1, {2 L, 2 L}];]
Out: {0.0040002, Null}
In:AbsoluteTiming[SparseArray[{i_u,i_u}/;i 1,{2 L,2 L}];]
输出:{0.0040002,Null}
我可以继续谈一些其他的事情,但我担心如果我这样做,我会把这个答案说得过于夸张。在我尝试优化一些代码的过程中,我积累了许多形成这些不同矩阵和列表的技术。我使用的基本代码运行一次计算需要6天以上的时间,现在只需要6小时就可以完成同样的工作
我会看看是否能找出我想出的一般技巧,然后把它们放在笔记本上使用
TL;DR:似乎在这些情况下,功能性方法优于程序性方法。但编译时,过程代码的性能优于函数代码。查看
Compile
对Do
循环所做的工作很有启发性。考虑这一点:
L=1200;
Do[.7, {i, 1, 2 L}, {j, 1, i}] // Timing
Do[.3 + .4, {i, 1, 2 L}, {j, 1, i}] // Timing
Do[.3 + .4 + .5, {i, 1, 2 L}, {j, 1, i}] // Timing
Do[.3 + .4 + .5 + .8, {i, 1, 2 L}, {j, 1, i}] // Timing
(*
{0.390163, Null}
{1.04115, Null}
{1.95333, Null}
{2.42332, Null}
*)
首先,假设Do
的参数超过一定长度(如Map
,Nest
等)时,它不会自动编译:您可以继续添加常量,所用时间与常量数量的导数是常量。SystemOptions[“CompileOptions”]
中不存在这样的选项,这进一步支持了这一点
接下来,由于循环次数为n(n-1)/2次n=2*L
,因此我们的L=1200
循环次数约为3*10^6次,每次添加所需的时间表明所进行的操作远远超出了必要的范围
接下来让我们试试
Compile[{{L,_Integer}},Do[.7,{i,1,2 L},{j,1,i}]]@1200//Timing
Compile[{{L,_Integer}},Do[.7+.7,{i,1,2 L},{j,1,i}]]@1200//Timing
Compile[{{L,_Integer}},Do[.7+.7+.7+.7,{i,1,2 L},{j,1,i}]]@1200//Timing
(*
{0.032081, Null}
{0.032857, Null}
{0.032254, Null}
*)
所以这里的情况更合理。让我们来看一看:
Needs["CompiledFunctionTools`"]
f1 = Compile[{{L, _Integer}},
Do[.7 + .7 + .7 + .7, {i, 1, 2 L}, {j, 1, i}]];
f2 = Compile[{{L, _Integer}}, Do[2.8, {i, 1, 2 L}, {j, 1, i}]];
CompilePrint[f1]
CompilePrint[f2]
两个CompilePrint
s给出相同的输出,即
1 argument
9 Integer registers
Underflow checking off
Overflow checking off
Integer overflow checking on
RuntimeAttributes -> {}
I0 = A1
I5 = 0
I2 = 2
I1 = 1
Result = V255
1 I4 = I2 * I0
2 I6 = I5
3 goto 8
4 I7 = I6
5 I8 = I5
6 goto 7
7 if[ ++ I8 < I7] goto 7
8 if[ ++ I6 < I4] goto 4
9 Return
我不会显示完整的清单,但在第一个清单中有一行R3=Sin[R1]
,而在第二个清单中有一个寄存器赋值R1=0.43496553411123023
(但是,它在循环的最内部被R2=R1
重新赋值;如果我们输出到C,它最终将由gcc优化)
因此,在这些非常简单的情况下,未编译的Do
只是盲目地执行主体,而Compile
执行各种简单的优化(除了输出字节码)。在这里,我选择的例子夸大了Do
对其论点的字面解释,而这种事情部分解释了编译后的巨大加速
至于速度,我认为在这些简单的问题中(仅仅是循环和乘法)的加速是因为没有理由自动生成的C代码不能被编译器优化以使事情尽可能快地运行。产生的C代码对我来说太难理解了,但是字节码是可读的,我不认为有任何浪费。的确如此
1 argument
9 Integer registers
Underflow checking off
Overflow checking off
Integer overflow checking on
RuntimeAttributes -> {}
I0 = A1
I5 = 0
I2 = 2
I1 = 1
Result = V255
1 I4 = I2 * I0
2 I6 = I5
3 goto 8
4 I7 = I6
5 I8 = I5
6 goto 7
7 if[ ++ I8 < I7] goto 7
8 if[ ++ I6 < I4] goto 4
9 Return
f5 = Compile[{{L, _Integer}}, Block[{t = 0.},
Do[t = Sin[i*j], {i, 1, 2 L}, {j, 1, i}]; t]];
f6 = Compile[{{L, _Integer}}, Block[{t = 0.},
Do[t = Sin[.45], {i, 1, 2 L}, {j, 1, i}]; t]];
CompilePrint[f5]
CompilePrint[f6]