问题 在Mathematica中构造大块矩阵的最有效方法是什么?


受到Mike Bantegui的启发  在构造定义为递归关系的矩阵时,我想知道在最少的计算时间内是否有任何关于设置大块矩阵的一般指导。根据我的经验,构建块然后将它们放在一起可能效率很低(因此我的答案实际上比Mike的原始代码慢)。 Join 可能 ArrayFlatten 可能效率低于它们。

显然,如果矩阵稀疏,可以使用 SparseMatrix 构造,但有时你构造的块矩阵不稀疏。

这类问题的最佳做法是什么?我假设矩阵的元素是数字。


7219
2017-07-28 23:40


起源

只是用 Do 循环和编译可以非常快,特别是在版本8中,它提供了编译为C的选项。但我对此没什么经验所以不能提供一般意见 - acl
我必须在这里同意acl。天真 Do 循环实际上比功能版本更快,特别是对于大矩阵大小。编译时, Do 循环变得明显更快。一件有趣的事情就是这样的事实 a + b,其中a和b是矩阵,真的很慢。 - Mike Bailey
是的,这就是我提出这个问题的原因。长期以来,Mathematica用户都被称为“对循环说不”,并认为函数式编程风格在Mathematica中更有效。当人们抱怨Mma比Matlab慢时,这就是股票反应。但是,如果最近的版本发生了变化,我认为值得记录下来。 - Verbeia
@Verbeia:我同意。我的回答实际上有一些相当有趣的例子说明了这一点。我下面使用的所有例子都不是任意的玩具示例。所有这些都在我的生产代码中用于一些相当长且昂贵的计算。 - Mike Bailey
我在书中讨论了一个你可能感兴趣的例子,这里: mathprogramming-intro.org/book/node553.html (引言在网络版中缺少,但它出现在pdf中)。我正在生成具有一定对称性的块随机矩阵,在该示例中,我比较了不同的技术。然而问题有点不同:在我的情况下,每个矩阵不一定非常大,但我不得不生成大量的矩阵。此外,我在M5-M6中做到了,早在编译到C之类的东西可用之前。 - Leonid Shifrin


答案:


下面显示的代码可在此处获得: 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:似乎对于这些情况,功能方式优于程序方式。但是在编译时,过程代码优于功能代码。


8
2017-07-29 00:54





看什么 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 如果超过一定长度,则不会自动编译其参数(如 MapNest 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。不过,事情可能会变得非常棘手。

但这太长了。


4
2017-07-29 22:24