记录一些常用的MATLAB编程技巧/规范,目标是写出高效并且可维护的MATLAB代码。主要参考官方文档中的提升性能的方法

这里的讨论只关注运算效率,假设内存总是足够的,并且不涉及并行计算和GPU。

关于代码结构

我们有很多方式执行代码:命令行 vs 脚本 vs 函数,虽然绝大多数代码在不同方式中执行都是等效的,但是考虑优化就不是一回事了,通常的运行效率关系为:命令行 < 脚本 < 函数,也就是说函数的执行速度通常是最快的。

基于模块化编程的思维,避免使用过大的单一文件,考虑将其中重复使用的功能拆分为简洁的函数文件,这种做法可以降低首次运行的成本。

优先使用局部函数而非嵌套函数,嵌套函数可以直接访问外层函数的变量(按照引用捕获),这会影响效率,更好的做法是将所有需要的变量以函数参数的形式显式传递。

关于数组操作

预分配+向量化

两个核心原则:预分配 + 向量化

  • 预分配数组:提前分配数组大小,避免在循环中动态扩展数组。
  • 向量化计算:尽可能使用矩阵和向量运算,避免 for 循环。

我们可以对 for 循环中的几种常见写法进行简单的对比:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
clc;
clear;
close all;

num = 1e6;
freq = 0.01;

% case 1
clearvars -except num freq;
tic
x = sin(2*pi*(1:num)*freq);
toc

% case 2
clearvars -except num freq;
tic
x = zeros(1,num);
for k = 1 : num
x(k) = sin(2*pi*k*freq);
end
toc

% case 3
clearvars -except num freq;
tic
for k = 1 : num
x(k) = sin(2*pi*k*freq);
end
toc

% case 4
clearvars -except num freq;
tic
x = [];
for k = 1 : num / 5
x = [x sin(2*pi*k*freq)];
end
toc

在我的电脑上测试结果如下

1
2
3
4
Elapsed time is 0.003326 seconds.
Elapsed time is 0.014658 seconds.
Elapsed time is 0.061899 seconds.
Elapsed time is 15.069429 seconds.

很显然,第一种完全向量化的写法是最优的,计算效率是最高的, 第二种写法对数组的整个空间进行了预分配,在for循环中对每一个元素赋值,效率虽然比不了向量化的写法,但是比后两种需要不断扩容数组的写法要快很多。

至于最后的两种写法:

  • 第三种写法性能比较低,因为每次都在向量中添加一个元素,尺寸在不断扩充,但是扩容都是就地进行的,由于预留空间的存在,并不是每次扩容都触发了整个数组的拷贝,因此实际效率并没有想象的那么糟糕。
  • 第四种写法的性能毫无疑问是最差的(这里已经将循环次数改小了),因为它是通过构造新矩阵并赋值的方式完成的矩阵扩容效果,在每一次 for 循环中,x = [x y] 都对整个数组进行了完整的拷贝,这导致计算复杂度更高,效率慢的可怕。

稀疏数组

稠密数组和稀疏数组在计算效率和内存开销上有巨大差异,如果一个矩阵确实是稀疏的,那么应该使用稀疏矩阵存储, 至少确保在计算的主要环节中都将其存储为稀疏矩阵并处理,而不是转换为含有很多0的稠密矩阵。

优先使用内置函数

在可选择的情况下,优先考虑使用 MATLAB 的内置函数来实现数组的常用运算,因为内置函数可能是直接使用底层语言实现的,至少也经过了大量的优化,比我们直接实现的效率肯定更高。

关于变量

尽量避免使用全局变量,因为这会降低代码的性能。

虽然MATLAB的变量没有固定的类型,但是仍然应该尽量避免类型和形状的改变,直接创建新变量,而不是复用现有变量,因为改变现有变量的数据类型或形状会带来额外开销。

避免在代码中存储数据,例如硬编码超大规模的字面量矩阵(比如超过 500 行的数据),将数据存储为 .mat 或 .csv 文件,在代码中通过命令读取文件来生成矩阵,无论从什么角度都是更好的做法。

关于内置函数

避免重载内置函数,尤其不要对标准 MATLAB 数据类重载内置函数。

关于环境清理

调用 clear allclear classesclear functions 会降低代码性能,且通常没有必要。

建议的用法如下:

  • 要从当前工作区中清除一个或多个特定变量,可以逐个指定变量,使用 clear name1 ... nameN
  • 要清除当前工作区中的所有变量,可以使用 clearclearvars
  • 要清除所有全局变量,可以使用 clear globalclearvars –global
  • 要清除特定类,可以使用 clear myClass
  • 要清除特定函数,可以使用 clear functionName

避免使用动态函数

虽然MATLAB提供了很多基于字符串解析的代码执行/函数调用/变量修改功能,支持查询和修改当前工作区变量,但是它们会带来额外的开销,不利于代码优化,并且会降低代码的灵活性和可维护性。

例如:

  • 避免使用查询MATLAB状态的函数,例如 inputnamewhichwhosexist(var)dbstack 等具有显著动态性质的函数,运行时的自检会消耗大量计算资源。
  • 避免使用 evalevalcevalinfeval(fname) 等基于字符数组解析来执行的函数,从文本间接计算会降低运行效率,尽可能使用函数句柄代替。