在 Wolfram 语言的后续版本中引入了与本教程内容相关的附加功能. 最新信息请见矩阵和线性代数.

线性代数计算的性能

本教程涵盖与性能相关的问题.

压缩数组

建立 Wolfram 语言的挑战之一是确保系统具有足够的一般性来支持符号计算并具有足够快的速度来进行有效的数值计算. 关于这些及其它目标在 "Wolfram 语言的设计原理" 一节中介绍. Wolfram 语言提供的一项技术是对 Wolfram 语言表达式的重要类型使用专业化的表示. 对于线性代数,这涉及到版本 4.0 中首次提出的所谓压缩数组技术.

这个例子表示出一个实数向量:
现在在向量中插入一个符号量. 关于向量和矩阵更新的讨论请见"设置部分矩阵"一节:
当字节数被度量时,该向量显示使用更多的内存:
另外,关于向量的数学运算要慢得多:

第一个向量使用的内存较少,支持更快的计算,原因是它是由压缩数组构建的. 压缩数组代表的是机器整数、机器实数以及整数和虚数部分均为机器实数的复数的矩形张量.

机器尺寸整数
机器精度实数
机器精度复数

压缩数组类型.

有许多函数对于压缩数组非常有用. Developer`PackedArrayQ 测试其参数是否是一个压缩数组. 这里表明 Table 的计算结果是一个压缩数组:
如果在该数组中插入一个实数,结果仍是压缩数组:
压缩数组的计算结果仍是压缩数组:
然而,如果在该数组中插入一个一般的符号量,结果将不再是压缩数组:
压缩数组的所有元素必须属于同一类型. 因此,如果整数0被插入一个实数压缩数组,结果将不再是压缩数组:

压缩数组函数

这里对用于压缩数组的一些函数作一总结.

Developer`PackedArrayQ[expr]expr 是压缩数组则返回 True
Developer`ToPackedArray[expr]尽可能将 expr 转化为压缩数组
Developer`ToPackedArray[expr,t]尽可能将 expr 转化为类型为 t 的压缩数组
Developer`FromPackedArray[expr]将压缩数组转化为 expr
Developer`PackedArrayForm[expr]以摘要形式打印 expr 的压缩数组

压缩数组函数.

函数 Developer`ToPackedArray 可用于生成一个压缩数组:
如果列表中元素的类型不尽相同,则结果将不是一个压缩数组:
通过指定类型 Real,整数被强制转化为实数,结果是压缩数组:
这里表明 pack 是由3个实数组成的压缩数组:
这不会生成一个压缩数组,因为参数不是矩形张量:

压缩数组运算

压缩数组的重要一点是,除了"压缩矩阵函数"中所介绍的压缩数组专用函数外,压缩数组的工作方式与列表完全相同. 例如,将 SameQ 应用于一个列表的压缩形式和非压缩形式将返回 True
压缩形式和非压缩形式在模式匹配程序中的表现相同:

与非压缩列表相比,许多函数在压缩数组的应用更为有效,但结果将相同,不管所用的是压缩或非压缩形式. 为了确保前后一致,有时系统必须要将压缩数组进行解压缩. 由于这将是导致低效的潜在原因,可有一则信息来告知用户什么已被解压缩. 通过一个系统选项启用这项功能.

这里设置系统选项并创建一个压缩数组来作为范例:
现在,如果一个实数被插入压缩数组,该数组必须被解压缩,并且生成提示信息. 该信息可以警告用户其代码可能不会有效运行:
把消息再次关闭通常是一个好主意:

压缩数组总结

关于压缩数组的主要一点是, Wolfram 语言会自动在适当时候使用它们. 对于自然生效于相同内部表示的线性代数函数尤为如此. 因此,许多函数在即使输入不是压缩数组的情况下仍返回压缩数组. 例如,函数 Dot 返回一个压缩数组,因为它在内部是运行于压缩数组:

通常很难利用诸如 Developer`ToPackedArray 等函数来启用压缩数组. 主要问题是确保矩阵中各元素的一致性,例如确保不要将整数和实数混合.

编程效率

本节讨论如何编写高效的 Wolfram 语言程序来求解线性代数问题.

性能的度量

如果您想研究自己编写的 Wolfram 语言程序代码的性能,有一些与时间相关的函数可以使用.

Timing[expr]计算 expr 并返回 CPU 所需时间和结果的列表
AbsoluteTiming[expr]给出所用的绝对时间,计算 expr

Wolfram 语言的计时运算.

函数 Timing 尤其有用,因为它返回的是计算所用的 CPU 时间:

另一个有用的函数是 TimeConstrained.

TimeConstrained[expr,t]尽量计算 expr,在 t 秒后停止计算
TimeConstrained[expr,t,failexpr]如果没有达到时间限制,返回 failexpr

限制时间的计算.

这有助于开发程序代码并进行测试,如果所需时间超过了一个预先设定的限制,则将其关闭:

循环向量化

常见的一种运算涉及到在矩阵的元素上进行某种类型的迭代. 由于矩阵可能很大,效率显然是至关重要的.

编写高效代码最重要的方式之一就是要避免显式部分引用,特别是在内部循环中. 这是用户代码效率低下的主要来源,使用同时作用在矩阵或向量的所有元素上的函数可以避免这种问题. 这些向量化的操作允许 Wolfram 语言使用高度优化的代码,运行速度更快.

创建列表

这个问题涉及到创建一个列表,然后用一定值进行初始化. 例如,计算

        vi  Sin[i]

一种方法是创建列表然后使用一个 For 循环在各个元素上进行迭代从而对这些元素进行赋值.

    n = 10;    
    v = Table[0, {n}];
    For[i = 1, i <= n, i++, v[[i]] = Sin[2.0 Pi i]];

初始化列表的缓慢方式.

创建列表与初始化同步进行将快得多,并且更加简洁.

    n = 10;
    v = Table[ Sin[2.0 Pi i], {i, 1, n}]

初始化列表的较快方式.

一种更为快捷的方式是使用优化函数 Range 来创建列表,然后使用向量化的运算.

    n = 10;
    v = Sin[2.0 Pi Range[1., n]]

初始化列表的一种更快方式.

用于创建列表和矩阵的诸如 TableRange 等高度优化的命令将在 "构建矩阵" 中讨论.

更新列表

这个问题涉及到列表元素的更新. 例如,计算每个元素的 Sin.

        vi  Sin[vi]

一种方法涉及在列表的每个元素上进行迭代,将每个元素用更新后的元素替换.

    Do[v[[i]] = Sin[v[[i]]], {i, n}];

更新列表的缓慢方式.

通过使用向量化的运算计算新值将快得多,并且代码更加简洁.

    v = Sin[v]

更新列表的较快方式.

另一种更新形式要求另外一个参数,该参数在列表内并不恒定.

        vi  vi/i^2

解决这个问题的一种方法涉及到在列表的元素上进行迭代,将每个元素除以索引的平方.

    n = Length[v];
    Do[ v[[i]] = v[[i]]/i^2 , {i, n}];

更新列表的缓慢方式.

先计算更新列表然后进行向量化的除法速度较快.

    d=Range[n]^2;
    v=v/d

更新列表的较快方式.

可列表运算的使用在"可列表性" 中讨论.

使用内置支持

Wolfram 语言具有范围非常广泛的函数,包括许多在矩阵计算中有重要应用的列表处理函数. 在开始工作之前,查看一下是否有内置函数能够帮您实现目的是很有必要的. 例如,如果所进行的工作涉及卷积或相关,那么应该使用适当的函数,这些函数几乎肯定会更快.

另外要注意的重要一点是,Wolfram 语言能够表示范围广泛的数学对象. 例如,Wolfram 语言能用于解决实际的线性方程问题,这些线性方程由矩阵来表示. 有时,方程形式的表述性更强也更方便,从而导致效率提高. 这个主题将在"将方程转化为稀疏矩阵"中讨论.

列表的结构化操纵是应用于许多线性代数计算的内置函数的一个领域. 诸如 Part, Take, Drop, Transpose, PadLeft, PadRight, RotateLeftRotateRight 等 Wolfram 语言函数是经过高度优化的,并有众多应用. 有关于此的更多细节将在"结构化运算"中讨论. 如果您可以编写自己的计算以使用这些函数,通常将是非常理想的.

现在给出一个例子来讨论通过平铺复制来拓展矩阵的不同方法.例如,通过重复填充输入矩阵

            

可返回下述矩阵.

            

下面说明解决该问题的三种不同方式.

一种缓慢的方法

最慢的方法是直接操纵每个元素,一步一步来写出矩阵:

这种方法存在几种速度上的缺陷,最严重的是在部分索引上进行内循环. 如果替代以向量化代码,速度将大大加快. 在"性能的度量"中已经解释过在内部循环中进行显式部分引用,将导致代码的低效.

应注意的一点是,当稀疏矩阵作为输入被给出时,该函数仍然有效,但返回的将是一个密集矩阵:

一种较快的方法

复制矩阵的一种快得多的方法是使用 Wolfram 语言的向量化函数:

这种方法要快得多;它使用函数的 Part 非常灵活并经过了高度优化. 这将在"得到矩阵的一部分"中进一步介绍.

这个函数用于一个输入的稀疏矩阵,返回的结果是一个保留了等价稀疏度的稀疏矩阵:

速度快并且更简洁

另外一种不需编程使矩阵扩展的方法是使用内置函数 PadRight

这种方法也更加简洁,并且对于小型输入矩阵来说可能是最快的. 对于大型矩阵,其速度与已经介绍过的 Part 范例中的速度相似. PadRight 的用法在"矩阵的扩展"中介绍.

这个函数用于一个输入的稀疏矩阵,返回的结果是一个保留了等价稀疏度的稀疏矩阵:

矩阵内容

Wolfram 语言中的矩阵可以由 Wolfram 语言所持有的所有不同类型对象构建. 这些矩阵可以包含机器精度浮点数、任意精度浮点数、复数浮点数、整数、有理数以及一般的符号量. 这些不同类型在"矩阵类型"中讨论.

Wolfram 语言可以操作这些不同类型的矩阵,这一点给系统强大的力量. 但是,它对数值浮点计算的效率有一些影响:首先来自于符号和数值项的组合;其次来自于不同类型数字的组合;第三来自于整数矩阵.

混合的符号/数值矩阵

如果在一个矩阵中将符号型和数值型元素混合,线性代数函数将如"混合模式矩阵"中所讨论的,把该矩阵视为符号矩阵. 应该注意的是,操作符号矩阵的方法通常比操作数值矩阵要慢,因为对于数值矩阵,有许多高度优化的技术可用.

这个例子将两个200×200矩阵进行矩阵乘法作比较. 第二个计算涉及到符号矩阵,速度要慢得多:

该解是为了确定您的矩阵只含有数字.

混合数值类型矩阵

如果矩阵中混合了不同的数值类型,线性代数函数将把矩阵视为数值型,并将矩阵强制转换为最低的精确度. 这在"混合模式矩阵"中讨论. 对于线性代数函数,这样做对性能所带来的影响将不会像处理符号矩阵那么严重(这一点在前面一节中已经说明).

这个例子比较两个200×200矩阵的矩阵乘法. 第二个矩阵含有一个整数,进行矩阵/矩阵相乘的速度较慢. 如果该操作更加耗费资源的话,这种差别可能不是很大:
混合数值矩阵代价的来源是,Wolfram 语言不能使用其高效的存储技术,这一点曾在"压缩数组"一节中所讨论过. 这在下面的例子中进行说明. 这里分配了一个数字向量,并被某种进程所更新. 由于更新的值将是实数,而初始向量包含整数,这个进程不能使用压缩数组:
在这个版本中,向量用实数初始化. 现在整个计算都能够使用压缩数组,运行速度更快:

这个问题的解决办法是,如果将要进行实数的操作,一定要确保使用实数进行初始化.

整数矩阵

最后一类问题涉及到纯整数矩阵的操作. Wolfram 语言将整数矩阵和近似实数矩阵的操作区别开来,这在"Wolfram 语言中的矩阵"中讨论. 由于一个整数矩阵将使用符号计算技术,其速度较慢. 当然,如果对于整数矩阵 Wolfram 语言不使用整数矩阵技术的话,它将不会具有如此广泛的通用性:
可以比较两种计算的不同. 整数矩阵返回的特征值是用根式表示的精确值. 实数矩阵返回的特征值用近似实数值表示:

当然,Wolfram 语言的一个强项是,它能够对涉及整数矩阵的计算返回精确结果. 但应该注意的是,许多纯数值系统将返回近似结果.

这类问题的解决方法是,如果进行的是关于近似实数矩阵的操作,应该以近似实数开始.

表达式效率

"用作 Wolfram 语言表达式的矩阵" 对 Wolfram 语言用表达式贯穿整个系统的一种统一的数据类型的形式表示矩阵做出了解释. 它介绍了系统使用这种统一格式的优越性. 本节在此讨论的是使用 Wolfram 语言表达式对效率带来的一些影响.

矩阵的更新

Wolfram 语言表达式的一个一般原则是这些表达式不能直接被改变. 表达式的这种不变性非常有用,它使表达式能够在许多地方共享,而每次使用都不必担心代码的其他部分将改变该表达式. 这意味着,当一个表达式更新(这在"设置部分矩阵"中介绍)时,可能需要作一个副本. 在这个例子中,创建了一个向量并分配给两个不同的符号:
现在改变一个符号的内容:
而另外一个保持不变. 这是由于表达式在修改之前被复制:
这种复制对效率有一定影响,尤其是当您对矩阵中的元素进行迭代并更新时. 如果这项任务完成,而您又有矩阵的其它副本时,它将需要被复制. 这一原则在下面两个例子中进行示范. 在第一个例子中,没有额外的向量副本,因此不需进行复制,循环运行较快:
这个例子中有一些额外的向量副本,因此必须在每一步进行复制;因此循环过程更慢:
另外一种更新元素的方法是使用 ReplacePart. 这同样要在每一步进行复制:

使用的是先前介绍的向量化运算,能够避免在矩阵上进行迭代,并将对运算的复制次数最小化. 如果在矩阵上的迭代无法避免,一个明智的方法是尽可能的使循环简洁从而避免额外复制矩阵.

整体过程所需时间与在每一步采用复制方法的列表长度的平方成比例. 如果不进行复制,则时间分配是线性的. 输入列表越长,影响越大,并且代码的性能将开始降低.

矩阵的增补

在 Wolfram 语言中,表达式按数组方式执行. 这允许快速访问表达式的任何具体内容. 由于矩阵是正规的 Wolfram 语言 表达式,它们也是数组. 虽然数组元素的访问速度非常快,添加元素却较慢. 这通常需要一个副本,这种情况应该避免. 后面的例子说明了增加元素的代价.

一次产生整个向量效率要高得多:
如果参数不能同时获知,可以采用其它方法. 这些方法将在下面说明. 首先构建一个随机数列表:
第一种方法使用的是 AppendTo
插入空列表、将列表累积要快得多;插入的空列表能用 Flatten 在最后删除:
使用 Flatten 可能不方便,因为这可能影响到列表的结构. 在这种情况下,使用 SowReap 可能比较便利. 它比 Flatten 的方法更灵活:
这三种方法均生成相同的结果:

整体过程所需时间与在每一步采用复制方法的列表长度的平方成比例. 如果不进行复制,则时间分配是线性的. 输入列表越长,影响越大,并且代码的性能将开始降低.