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

时间:2011-07-28 23:40:43

标签: matrix wolfram-mathematica

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

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

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

2 个答案:

答案 0 :(得分:8)

下面显示的代码可在此处找到: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循环替换它,但我保留它因为看起来更干净。

原始Do循环的当前计数为9.06秒,使用OuterDiagonalMatrix的下一个版本为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}

大部分速度来自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}

相反,让我们使用ArrayPadIdentityMatrix

进行构建
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:似乎对于这些情况,功能方式优于程序方式。但是在编译时,程序代码优于功能代码。

答案 1 :(得分:4)

查看CompileDo循环的作用是有益的。考虑一下:

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等),则不会自动编译其参数:您可以继续添加常量和时间的推导与常数的关系是不变的。 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给出相同的输出,即

        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的分配(但是,在{1}}中重新分配循环的最里面部分是R2 = R1;也许如果我们输出到C,这将最终由gcc优化。)

因此,在这些非常简单的情况下,未编译的Do只是盲目地执行正文而不检查它,而Compile确实进行了各种简单的优化(除了输出字节代码)。在这里,我选择的例子夸大了Do解释其论点的方式,这种事情部分解释了编译后的大加速。

对于huge speedup in Mike Bantegui's question yesterday,我认为这些简单问题(只是循环和倍增)的加速是因为没有理由不能通过编译器优化自动生成的C代码来使事情运行尽可能快地。生成的C代码对我来说太难理解,但字节码是可读的,我不认为有任何浪费。因此,当编译为C时,它的速度是如此之快并不令人震惊。使用内置函数不应该比这更快,因为算法中应该没有任何差别(如果有的话,{{1不应该以这种方式编写循环)。

当然,应该逐案检查所有这些。根据我的经验,Do循环通常是进行此类操作的最快方法。但是,编译有其局限性:如果要生成大型对象并尝试在两个编译函数(作为参数)之间传递它们,则瓶颈可能就是这种转移。一种解决方案是简单地将所有内容放入一个巨大的函数中并编译它;这最终变得越来越难(你被迫在mma中写C,可以这么说)。或者您可以尝试使用Do中的CompilationOptions -> {"InlineCompiledFunctions" -> True}]编译各个函数。不过,事情可能会变得非常棘手。

但这太长了。