MATLAB 高性能编程笔记
记录一些常用的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
38clc;
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
4Elapsed 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 all
、clear classes
和
clear functions
会降低代码性能,且通常没有必要。
建议的用法如下:
- 要从当前工作区中清除一个或多个特定变量,可以逐个指定变量,使用
clear name1 ... nameN
。 - 要清除当前工作区中的所有变量,可以使用
clear
或clearvars
。 - 要清除所有全局变量,可以使用
clear global
或clearvars –global
。 - 要清除特定类,可以使用
clear myClass
。 - 要清除特定函数,可以使用
clear functionName
。
避免使用动态函数
虽然MATLAB提供了很多基于字符串解析的代码执行/函数调用/变量修改功能,支持查询和修改当前工作区变量,但是它们会带来额外的开销,不利于代码优化,并且会降低代码的灵活性和可维护性。
例如:
- 避免使用查询MATLAB状态的函数,例如
inputname
、which
、whos
、exist(var)
、dbstack
等具有显著动态性质的函数,运行时的自检会消耗大量计算资源。 - 避免使用
eval
、evalc
、evalin
和feval(fname)
等基于字符数组解析来执行的函数,从文本间接计算会降低运行效率,尽可能使用函数句柄代替。