自己使用MATLAB已有两年多的时间,虽然基本都能满足各种功能需求,但作为一个通用计算平台,MATLAB实际上有很多特定用途的工具箱和加速运算的小技巧。这些小技巧与学习语法或函数用法不同,更像是一种经验,需要不断积累。本文主要讲述如何优化MATLAB程序的运行速度。

最近,我在进行基于MATLAB的神经网络调参训练时,需要尝试很多超参数的不同组合。因此,在不同的机器上分别进行多种组合实验,以加速算法验证。然而,我发现在某些电脑加了内存条之后,运行速度反而变慢。这种变慢现象与MATLAB版本、电脑内存和CPU性能(运算能力与频率)都有关系。如果搭配不好的话,还不如低版本的MATLAB和较小内存的CPU配置。

接下来,我将从以下几种情况分析MATLAB在运行中变慢的问题:

1. MATLAB长时间运行越来越慢的问题

经常使用MATLAB进行计算的人可能都有这种感觉:在程序刚开始运行时速度很快,但随着运行时间的增加,速度逐渐变慢。实际上,我认为这个问题与CPU关系不大,主要与内存有关。具体问题的根源尚不清楚。不过,我可以提供两个建议来缓解这种问题:

- 将程序分解成多次运行,尽量避免一次运行过长时间;

- 单次大内存消耗的MATLAB程序运行完后将其关闭,下次运行时重新开启;

- 对于多次大内存消耗的MATLAB程序,有条件的话最好重启计算机;

- 将大内存消耗的MATLAB程序进行混合编程(包括将m文件转成C++可用的库,或者用C++/C写然后转成MATLAB可用的函数文件);如果可以直接用C/C++重新编写。

实际上,MATLAB也可以进行混合编程。通过使用mex命令编译,结合C语言程序的速度优势,或者调用特定的运行库、并行线程、增加运行核心与频率、加大内存等,甚至还可以借助CUDA代码进行GPU加速。

此外,还有其他问题和解决方案:

- 问题:我用MATLAB做了一个优化的程序,调用comsol来进行计算。但是运行时间较长。当我用实验室的服务器进行计算时,一晚上过后MATLAB的计算速度逐渐变慢,最后竟然停止了运算。请问这是怎么回事?

- 方案:请参考上述针对长时间运行变慢问题的解决方案。

你好,我可以帮你重构这段话。以下是重构后的内容:

很久没有上论坛了,刚看到你的回复。去年我参加了一个研讨会,一个工程师告诉我说如果matlab运行时间过长,可能每次运算所遗留的中间数据或者句柄(comsol和matlab link会在每次计算后保存句柄 heap),解决的方法是将matlab长时间的运行改为多个短时间的运行,在一次运行结束时保存好这次的数据,然后进行下一次运算时将上次的保存数据载入即可。在运行间隔的时候可以重启一下电脑或者matlab,以释放内存中的数据。

关于for循环过大导致程序运行慢的问题,我们都知道,写for循环在逻辑上以及运行速度上都需要权衡。对于大循环,一般要求尽量写在里面,小循环写在外面。参考如下文章:[1] [2]。

另外,如果你遇到了MATLAB启动慢的问题,可以尝试按照以下方法解决:

- 将MATLAB启动初始化很慢的原因定位为找注册文件。

- 在快捷方式里面目标路径中键入正确的路径和许可文件。

关于什么是“Performance Acceleration”,请参阅MATLAB的帮助文件。以下是对其规则的简要总结:

1. 只有使用以下数据类型,MATLAB才会对其加速:逻辑型、字符型、int8、uint8、int16、uint16、int32、uint32、双精度浮点数。如果语句中使用了非以上的数据类型,如:数值型、单元格数组、结构体、单精度浮点数、函数句柄、Java类、用户自定义类、int64和uint64,则不会加速。

2. MATLAB不会对超过三维的数组进行加速。

3. 当使用for循环时,只有遵守以下规则才会被加速:a) for循环的范围只用标量值表示;b) for循环内部的每一条语句都要满足以上的两条规则,即只使用支持加速的数据类型,只使用三维以下的数组;c) 循环内只调用了内建函数(build-in function)。

4. 当使用if、elseif、while和switch时,其条件测试语句中只使用了标量值时,将会加速运行。

5. 不要在一行中写入多条操作,这样会减慢运行速度。例如,不要有这样的语句:“x = a.name; for k=1:10000, sin(A(k)), end;”。

6. 当某条操作改变了原来变量的数据类型或形状(大小,维数)时,将会减慢运行速度。

7. 应该这样使用复常量:“x = 7 + 2i”,而不应该这样使用:“x = 7 + 2*i”,后者会降低运行速度。

二、 遵守三条规则

1. 尽量避免使用循环。MATLAB的设计初衷是作为矩阵语言,专注于向量和矩阵操作。可以通过利用向量化算法来加速您的M文件代码。向量化意味着将for和while循环转换为等效的向量或矩阵操作。改进这种情况有两种方法:

、尽量用向量化的运算来代替循环操作。如将下面的程序:

```matlab

for i = 1:N

for j = 1:M

result(i, j) = A(i, j) + B(i, j);

end

end

```

替换为:

```matlab

result = A + B;

```

速度将会大大加快。最常用的使用vectorizing技术的函数有:All、diff、ipermute、permute、reshape、squeeze、any、find、logical、prod、shiftdim、sub2ind、cumsum、ind2sub、ndgrid、repmat、sort、sum 等。请注意matlab文档中还有这样一句补充:“Before taking the time to vectorize your code, read the section on Performance Acceleration. You may be able to speed up your program by just as much using the MATLAB JIT Accelerator instead of vectorizing.”。何去何从,自己把握。

b、在必须使用多重循环时下,如果两个循环执行的次数不同,则在循环的外环执行循环次数少的,内环执行循环次数多的。这样可以显著提高速度。

2、a、预分配矩阵空间,即事先确定变量的大小,维数。这一类的函数有zeros、ones、cell、struct、repmat等。

b、当要预分配一个非double型变量时使用repmat函数以加速,如将以下代码:

```matlab

A = zeros(1000, 1000);

B = ones(1000, 1000);

C = A + B;

```

换成:

```matlab

A = repmat(zeros(1000), 1, 1000);

B = repmat(ones(1000), 1, 1000);

C = A + B;

```

c、当需要扩充一个变量的大小、维数时使用repmat函数。

3、a、优先使用matlab内建函数,将耗时的循环编写进MEX-File中以获得加速。b、使用Functions而不是Scripts。

三、绝招:

你也许觉得下面两条是屁话,但有时候它真的是解决问题的最好方法。

1、改用更有效的算法。

2、采用Mex技术,或者利用matlab提供的工具将程序转化为C语言、Fortran语言。关于如何将M文件转化为C语言程序运行,可以参阅本版帖子:“总结:m文件转化为c/c++语言文件,VC编译”。

要计算代码段的运行时间,可以使用MATLAB中的tic和toc方法。在代码段开始处添加tic,在结束处添加toc,MATLAB就能计算出这一代码段的运行时间。但是,tic和toc方法存在两个问题:

1. 显示的时间是运行时间“wall clock”,这个时间受你在运行你的代码时,你的计算机是否同时运行其他程序的影响。

2. 你需要不断地压缩计时范围来查找你代码运行最慢的位置。

一个最好的方法是利用MATLAB内嵌的代码分析器。在你的程序前面通过添加命令profile on;及在程序结束添加profile viewer;并运行你的程序。当程序正常运行结束时,代码分析器窗口将弹出,并显示分析结果。它包含的信息有:

- Function Name:函数名;

- Calls:函数被调用次数;

- Total Time:执行该函数的CPU总用时,包含任何其它被它调用的函数的CPU时间;

- Self Time:执行该函数的CUP总用时,不包含任何其它被它调用的函数的CUP时间;

- Total Time Plot:时间用时的曲线图。

以上信息可进行各种排序和详细查看。注意,当你完成代码分析后,请删除profile on和profile viewer,因为嵌入代码分析器会使用程序运行变慢。

另外,如果你使用的是多核心计算机,你可以利用MATLAB同时运行多个线程。Matlab程序中一些底层的函数就有可能采用并行计算的方法。打开多线程的方法是在File→Preferences中选择General→Multithreading,并勾选Enable multithread computation box。如果不限制使用核心数目,可以保留使用Automatic。需要注意的是,Matlab R2008a之前的版本在AMD处理器上是不支持多线程的。

向量化循环在Matlab中,运算主要针对向量(矢量)和矩阵进行设计,因此在向量和矩阵上的运算速度比采用循环的方式更快。例如:

```matlab

% 采用以下代码可加快速度

for i = 1:1000

sum(data(i))

end

```

一些有用的、可用于代替循环的函数有:

- `sum()`:计算数组元素之和。

- `mean()`:计算数组元素的平均值。

- `max()`:返回数组中的最大值。

- `min()`:返回数组中的最小值。

- `prod()`:计算数组元素的乘积。

- `std()`:计算数组的标准差。

- `var()`:计算数组的方差。

矢量化编程很重要!!!

向量预分配在Matlab中,采用内存中一块连续的空间来存储向量和矩阵数据,而不是用链表。这就意味着你每给向量或矩阵增加一元素,Matlab需要寻找一块足够大的内存区域来存储这个扩大后的向量或矩阵,然后复制现有的数据到新的内存域。在循环中增加向量或矩阵元素的元数是允许的,但并不是明智之举,而应该是一次性分配向量或矩阵的大小,或一次性重定义尺寸。

上述代码将比以下代码速度慢:

```matlab

% 注意:当你需要用zeros()来创建一个指定数据类型的向量或矩阵时,你可以使用创建参数来指定类型,而不是“重铸”。results=int8(zeros(1,1000));将创建一个有1000个元素的double型零向量,然后把它转换成int8类型。如果我们使用results=zeros(1,1000,‘int8’); Matlab将支持建立1000个int8类型的向量,在创建可实现性及速度上将更具有优势。

% 不要改变数据类型

% Matlab为了能够支持宽松的数据类型(例如一个变量能够存储不同类型的数据,而不是指定它为特定的数据类),则Matlab除了存储单纯的数据之外,还需要伴随数据存储一定数量的头信息(header),这就意味着需要内存空间支存储数据类型,同时意味需要在数据类型转换上支付额外的计算机资源开支。对于实数据使用 real...函数。Matlab中的一些函数能够同时适用于实类型数据和复类型数据。如果你只使用实数据,那么采用特定的版本的,非复数据函数,那么它运行的速度将变得更快。这些函数如:reallog(), realpow(),realsqrt()。

% 使用“短路”逻辑操作

% Matlab的“短路”逻辑操作可以在判断条件达到充分条件后就停止计算处理,而不需要知道判断所有条件。例如:if(index>=3)&&(data(index)==5); 当index小于3时,第二个条件判断将不被处理,这样就少了去判断data(index)==5)的时间,提高速度。

% 使用函数指针(句柄)

Matlab的一些函数使用函数名作为参数,通常用一个变量保存这个函数名字符串(如:func='tan'),然后用这个变量作为函数的参数:fzero(func,0))。这种方法对于简单的函数调用是很好的,但是对于在循环中的重复调用就存在两个问题:

1. 在每一个循环中,Matlab需要去搜索这个函数的路径(如tan),这需要花费时间。

2. 在循环过程中,路径可能会改变。这会保证在这一次循环中,某个版本的函数(如tan)被首先调用,而下一次循环中这个版本的函数又被首先调用,最终会造成结果不一致。

解决的办法是使用文件指针(;或func=@sin),它能返回函数唯一的识别码。调用方式同上。

文件I/O

通常高级输入输出操作(load()和save())比一般的低级操作(fread()和fwrite())快。

☆内存使用

关于内存的使用可查看帮助文档Using Memory Efficiently。可查与Memory Usage相关的信息。一定要注意:可以使用whos()来查看数据变量占有用的内存空间大小。

复制数组

当你复制一个数组时,Matlab开始只复制一个指向数据的一个指针,仅当你随后对任一版本进行修改时,数据的复制才真正的执行。这种操作包括数组作为函数参数进行传递的情况-作为值传递的参数传递,而不是作为参考的传递。因此,你应该尽量避开对大数组进行小改动的操作。

数据不用时,释放内存

如果一个变量以后已经不再使用,那么你可以删除它clear VariableName;则这个小块的数据将可以重用。注意:如果各变量在内存是连续的,则Matlab很容易重用这些大块的内存,因此最好是先建立大的变量,后再建立小的变量,并且把它们组合起来。

结构体存储

上文已经提到,在Matlab中的变量包含有描述数据类型的头信息。对于一个结构体,则有一个描述整个结构的头信息,及每个元素也分别有一个头信息。为了最小化地使用内存,我们应该小心地使用混合数据类型的数组和结构。例如:

```matlab

A = struct('a', [1 1], 'b', [2 2]); % 需要存储4个头信息。

B = struct('a', [3 3], 'b', [4 4], 'c', [5 5]); % 我们就有720001个头信息。

```

使用最小的合适的数据类型

为了减小内存使用量,对于特定的运算经常使用最小的数据类型。例如:

1. 对于虚部为零的数据,最好不要用complex去存储。

2. 如果精度足够,可采用single变量,而不用double。

3) 使用uint16进行计数操作,它能存储值为0到65535的数据。相较于默认的double型,它可以节省一半的内存空间。对于数组、元胞数组、结构体数组等占用内存的空间有很大差异。

(4) 当矩阵中大部分数据为零值时,可以考虑将其转换为稀疏矩阵(使用sparse()函数)。这样只会存储非零数据的数值和索引。但需要注意的是,由于需要额外存储数据的索引,只有二维数据的零值大约超过75%时,这种方法才是有效的,否则稀疏形式反而需要更多的内存空间。

(5) 并行循环:如果从外部观察一个for循环,满足以下条件:

- 循环计数为整数;

- 每次循环独立进行;

- 计算循环先后顺序无关。

那么这个for循环就有可能替换成parfor循环(matlab2008a中可用优化算打开并行通信池)。注意:打开一个并行工作池worker pool大约需要10-15秒钟,关闭一个工作池大概需要5秒钟。在这个时间范围内,对于循环时间超过30秒的情况,这种方法是值得尝试的。

此外,MATLAB对内建函数和自定义函数有一定的要求,尽量避免重名或具有相同文件名但不同后缀的函数。同时,在细节方面也可以进行优化。

(6) 提高代码运行速度的方法有以下几点:

- 使用matlab profiler(运行和计时)来分析每行代码所耗费的时间,并根据结果进行改进;

- 在生成随机数时,使用rand(x,y)函数生成均匀分布的随机数比其他分布的函数速度快很多;当需要生成非正态和均匀分布以外的随机数时,自定义函数通常比通用函数快得多,例如生成指数分布随机数时,exprnd(1,1,20)的速度要比random(‘exp’,1,1,20)快40多倍。

最后,如果你不想从算法的角度去加速代码运行,而是想充分利用内存资源的话,可以参考这篇博客,主要讲述如何扩大虚拟运行内存以及最大化节省内存的方法。