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

Matrix 在Mathematica中,构造大块矩阵最有效的方法是什么?,matrix,wolfram-mathematica,Matrix,Wolfram Mathematica,受Mike Bantegui关于构造一个定义为递归关系的矩阵的启发,我想知道是否有关于在最短计算时间内建立大型块矩阵的一般指导。根据我的经验,构建块然后将它们组合在一起可能效率很低(因此我的答案实际上比Mike的原始代码慢)Join和可能的ArrayFlatten的效率可能比它们低 显然,如果矩阵是稀疏的,则可以使用SparseMatrix构造,但有时要构造的块矩阵不是稀疏的 针对此类问题的最佳做法是什么?我假设矩阵的元素是数字。下面显示的代码在这里可用:。只需将其复制并粘贴到笔记本中进行测试。

受Mike Bantegui关于构造一个定义为递归关系的矩阵的启发,我想知道是否有关于在最短计算时间内建立大型块矩阵的一般指导。根据我的经验,构建块然后将它们组合在一起可能效率很低(因此我的答案实际上比Mike的原始代码慢)
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
循环来代替它,但我保留了它,因为它看起来更干净

对于naive
Do
循环,当前计数为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]