优化MATLAB代码指南

Sam*_*ley 8 optimization matlab

我已经注意到很多关于SO的问题,但没有一个很好的MATLAB优化指南.

常见问题:

  • 为我优化此代码
  • 我该如何对此进行矢量化?

我不认为这些问题会停止,但我希望这里提出的想法能让他们集中参考.

优化Matlab代码是一种黑色艺术,总有一种更好的方法.有时甚至无法对代码进行矢量化.

所以我的问题是:当矢量化不可能或极其复杂时,优化MATLAB代码的一些技巧和窍门是什么?此外,如果你有任何常见的矢量化技巧,我也不介意看到它们.

Sam*_*ley 14

前言

所有这些测试都是在与他人共享的机器上执行的,因此它不是一个非常干净的环境.在每次测试之间,我清除工作区以释放内存.

请不要注意个别数字,只需看看优化时间之前和之后的差异.

注:tictoc电话我都放在代码是显示我在哪里测量所花费的时间.

预分配

在Matlab中预先分配数组的简单行为可以提供巨大的速度优势.

tic;


for i = 1:100000
    my_array(i) = 5 * i;
end

toc;
Run Code Online (Sandbox Code Playgroud)

这需要47

tic;

length = 100000;
my_array = zeros(1, length);

for i = 1:length
    my_array(i) = 5 * i;
end

toc;
Run Code Online (Sandbox Code Playgroud)

这需要0.1018

添加一行代码47秒到0.1秒是一个惊人的改进.显然在这个简单的例子中你可以将它矢量化为my_array = 5 * 1:100000(花费0.000423秒),但我试图表示矢量化不是一个选项的更复杂的时间.

我最近发现零序函数(和其他相同性质的函数)在预分配时并不像简单地将最后一个值设置为0那样快:

tic;

length = 100000;
my_array(length) = 0;

for i = 1:length
    my_array(i) = 5 * i;
end

toc;
Run Code Online (Sandbox Code Playgroud)

这需要0.0991

现在显然这个微小的差异并不是很大,但你必须相信我在一个大文件中,其中许多优化的差异变得更加明显.

为什么这样做?

预分配方法为您分配一块内存供您使用.这个内存是连续的,可以预先获取,就像C++或Java中的数组一样.但是,如果你没有预先分配,那么MATLAB将不得不动态地找到越来越多的内存供你使用.据我了解,这与Java ArrayList的行为不同,更像是LinkedList,其中数组的不同块在内存中被分割.

当你向它写入数据时,这不仅会慢一些(47秒!),而且每次从那里开始访问它时速度也会变慢.实际上,如果您绝对无法预先分配,那么在开始使用之前将矩阵复制到新预分配的矩阵仍然很有用.

如果我不知道要分配多少空间怎么办?

这是一个常见问题,有几种不同的解决方案:

  1. 高估 -最好过度估计矩阵的大小并分配太多空间,而不是分配空间不足.
  2. 处理它并稍后修复 -我已经看到了很多开发人员忍受缓慢的人口时间,然后将矩阵复制到新的预分配空间.通常将其保存为.mat文件或类似文件,以便以后可以快速读取.

如何预先分配复杂的结构?

正如我们已经看到的那样,为简单数据类型预先分配空间很容易,但是如果它是一个非常复杂的数据类型,例如结构体结构呢?

我永远无法明确预先分配这些(我希望有人可以建议一个更好的方法)所以我想出了这个简单的黑客:

tic;

length = 100000;

% Reverse the for-loop to start from the last element
for i = 1:length
    complicated_structure = read_from_file(i);
end

toc;
Run Code Online (Sandbox Code Playgroud)

这需要1.5分钟

tic;

length = 100000;

% Reverse the for-loop to start from the last element
for i = length:-1:1
    complicated_structure = read_from_file(i);
end

% Flip the array back to the right way
complicated_structure = fliplr(complicated_structure);

toc;
Run Code Online (Sandbox Code Playgroud)

这需要6

这显然不是完美的预分配,之后需要一段时间来翻转阵列,但时间的改进不言而喻.我希望有人有更好的方法来做到这一点,但这是一个非常好的黑客同时.

数据结构

就内存使用而言,结构数组的数量级比结构数组差:

% Array of Structs
a(1).a = 1;
a(1).b = 2;
a(2).a = 3;
a(2).b = 4;
Run Code Online (Sandbox Code Playgroud)

使用624字节

% Struct of Arrays
a.a(1) = 1;
a.b(1) = 2;
a.a(2) = 3;
a.b(2) = 4;
Run Code Online (Sandbox Code Playgroud)

使用384字节

正如您所看到的,即使在这个简单/小的示例中,结构数组也比结构数组使用更多的内存.如果要绘制数据,阵列结构也是一种更有用的格式.

每个Struct都有一个大的头,正如你所看到的,结构数组多次重复这个头,其中数组的结构只有一个头,因此占用的空间更少.对于较大的阵列,这种差异更明显.

文件读取

freads您在代码中的数量(或任何系统调用)越少越好.

tic;    

for i = 1:100
    fread(fid, 1, '*int32');
end

toc;
Run Code Online (Sandbox Code Playgroud)

以前的代码比以下代码慢很多:

tic;
fread(fid, 100, '*int32');
toc;
Run Code Online (Sandbox Code Playgroud)

您可能认为这很明显,但同样的原则可以应用于更复杂的情况:

tic;

for i = 1:100
    val1(i) = fread(fid, 1, '*float32');
    val2(i) = fread(fid, 1, '*float32');
end

toc;
Run Code Online (Sandbox Code Playgroud)

这个问题不再简单,因为在内存中浮动代码如下所示:

val1 val2 val1 val2 etc.
Run Code Online (Sandbox Code Playgroud)

但是,您可以使用skipfread 的值来实现与以前相同的优化:

tic;

% Get the current position in the file
initial_position = ftell(fid);

% Read 100 float32 values, and skip 4 bytes after each one
val1 = fread(fid, 100, '*float32', 4);

% Set the file position back to the start (plus the size of the initial float32)
fseek(fid, position + 4, 'bof');

% Read 100 float32 values, and skip 4 bytes after each one
val2 = fread(fid, 100, '*float32', 4);

toc;
Run Code Online (Sandbox Code Playgroud)

所以这个文件读取是使用两个fread而不是200 来完成的,这是一个巨大的改进.

函数调用

我最近研究了一些使用了许多函数调用的代码,所有函数调用都位于不同的文件中.所以我们说有100个单独的文件,都互相调用.通过将此代码"内联"到一个函数中,我看到执行速度从9秒提高了20%.

显然你不会以牺牲可重用性为代价来做这件事,但在我的情况下,这些功能是自动生成的,根本不会重复使用.但我们仍然可以从中吸取教训,避免在不需要的情况下进行过多的函数调用.

外部MEX功能会产生被调用的开销.因此,对大型MEX函数的调用比对较小MEX函数的许多调用要高效得多.

绘制许多断开的线

在绘制断开连接的数据(如一组垂直线)时,在Matlab中执行此操作的传统方法是迭代多次调用lineplot使用hold on.但是,如果要绘制大量单独的线条,则会变得非常慢.

我发现的技术使用的事实是,您可以将NaN值引入数据以进行绘图,这将导致数据中断.

下面的设计示例将一组x_values,y1_values和y2_values(其中行从[x,y1]到[x,y2])转换为适合单次调用的格式plot.

例如:

% Where x is 1:1000, draw vertical lines from 5 to 10.
x_values = 1:1000;
y1_values = ones(1, 1000) * 5;
y2_values = ones(1, 1000) * 10;

% Set x_plot_values to [1, 1, NaN, 2, 2, NaN, ...];
x_plot_values = zeros(1, length(x_values) * 3);
x_plot_values(1:3:end) = x_values;
x_plot_values(2:3:end) = x_values;
x_plot_values(3:3:end) = NaN;

% Set y_plot_values to [5, 10, NaN, 5, 10, NaN, ...];
y_plot_values = zeros(1, length(x_values) * 3);
y_plot_values(1:3:end) = y1_values;
y_plot_values(2:3:end) = y2_values;
y_plot_values(3:3:end) = NaN;

figure; plot(x_plot_values, y_plot_values);
Run Code Online (Sandbox Code Playgroud)

我已经使用这种方法来打印数千条细线,性能改进非常大.不仅在初始绘图中,而且后续操作(如缩放或平移操作)的性能也得到了改善.