受到Mike Bantegui的启发 题 在构造定义为递归关系的矩阵时,我想知道在最少的计算时间内是否有任何关于设置大块矩阵的一般指导。根据我的经验,构建块然后将它们放在一起可能效率很低(因此我的答案实际上比Mike的原始代码慢)。 Join
可能 ArrayFlatten
可能效率低于它们。
显然,如果矩阵稀疏,可以使用 SparseMatrix
构造,但有时你构造的块矩阵不稀疏。
这类问题的最佳做法是什么?我假设矩阵的元素是数字。
受到Mike Bantegui的启发 题 在构造定义为递归关系的矩阵时,我想知道在最少的计算时间内是否有任何关于设置大块矩阵的一般指导。根据我的经验,构建块然后将它们放在一起可能效率很低(因此我的答案实际上比Mike的原始代码慢)。 Join
可能 ArrayFlatten
可能效率低于它们。
显然,如果矩阵稀疏,可以使用 SparseMatrix
构造,但有时你构造的块矩阵不稀疏。
这类问题的最佳做法是什么?我假设矩阵的元素是数字。
下面显示的代码可在此处获得: http://pastebin.com/4PWWxGhB。只需将其复制并粘贴到笔记本中即可进行测试。
我实际上是在尝试做几种计算矩阵的函数方法,因为我 认为功能方式(在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}
该 DiagonalMatrix
实际上比较慢。我可以用这个代替 Do
循环,但我保留它,因为它看起来更干净。
天真的当前计数是9.06秒 Do
循环,我的下一个版本使用1.389秒 Outer
和 DiagonalMatrix
。大约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}
大多数速度来自 CompilationTarget->"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}
相反,让我们使用它来构建它 ArrayPad
和 IdentityMatrix
:
In: AbsoluteTiming[ArrayPad[IdentityMatrix[k], {{0, 2L-k}, {0, 2L-k}}
Out: {0.0140008, Null}
这实际上不适用于k = 0,但你可以特殊情况,如果你需要它。此外,根据k的大小,这可以更快或更慢。它总是比Table [...]版本更快。
你甚至可以用这个来写 SparseArray
:
In: AbsoluteTiming[SparseArray[{i_, i_} /; i <= k -> 1, {2 L, 2 L}];]
Out: {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
do do):你可以继续增加常数,并且时间的导数与常数的数量是恒定的。这种选择的不存在进一步支持了这一点 SystemOptions["CompileOptions"]
。
接下来,因为这循环 n(n-1)/2
与时俱进 n=2*L
,所以我们的3 * 10 ^ 6倍左右 L=1200
,每次添加所花费的时间表明还有很多事情要发生。
接下来让我们试试吧
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
给出相同的输出,即
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
f1==f2
回报 True
。
现在,做
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]
我不会显示完整列表,但在第一行中有一行 R3 = Sin[ R1]
而在第二个是寄存器的分配 R1 = 0.43496553411123023
(但是,它在循环的最里面被重新分配 R2 = R1
;也许如果我们输出到C,这将最终由gcc优化)。
所以,在这些非常简单的情况下,未编译 Do
只是盲目地执行身体而不检查它,而 Compile
确实做了各种简单的优化(除了输出字节代码)。在这里,我选择的例子夸大了字面意思 Do
解释它的论点,这种事情部分解释了编译后的大加速。
至于 迈克班特吉昨天的问题加速了,我认为这些简单问题(只是循环和乘法)的加速是因为没有理由不能由编译器优化自动生成的C代码以使事情尽可能快地运行。生成的C代码对我来说太难理解,但字节码是可读的,我不认为有任何浪费。所以当编译为C时它是如此之快并不令人震惊。使用内置函数不应该比这更快,因为算法中应该没有任何差别(如果有的话, Do
循环不应该这样写)。
当然,所有这些都应该逐个检查。在我的经验中, Do
循环通常是进行此类操作的最快方法。但是,编译有其局限性:如果要生成大型对象并尝试在两个编译函数(作为参数)之间传递它们,则瓶颈可能就是这种转移。一种解决方案是简单地将所有内容放入一个巨大的函数中并编译它;这最终变得越来越难(你被迫在mma中写C,可以这么说)。或者您可以尝试编译各个功能并使用 CompilationOptions -> {"InlineCompiledFunctions" -> True}]
在里面 Compile
。不过,事情可能会变得非常棘手。
但这太长了。