前言
所有这些测试都是在与他人共享的机器上执行的,因此它不是一个完全干净的环境。在每次测试之间,我都会清除工作区以释放内存。
请不要关注个别数字,只看优化前后的时间差异。
注意:我在代码中调用的tic 和toc 是为了显示我在哪里测量所用时间。
预分配
在 Matlab 中预先分配数组的简单行为可以带来巨大的速度优势。
tic;
for i = 1:100000
my_array(i) = 5 * i;
end
toc;
这需要 47 秒
tic;
length = 100000;
my_array = zeros(1, length);
for i = 1:length
my_array(i) = 5 * i;
end
toc;
这需要 0.1018 秒
添加一行代码从 47 秒到 0.1 秒是一个惊人的改进。显然,在这个简单的示例中,您可以将其矢量化为 my_array = 5 * 1:100000(这需要 0.000423 秒),但我试图表示无法选择矢量化的更复杂的时间。
我最近发现 zeros 函数(以及其他性质相同的函数)在预分配方面不如简单地将最后一个值设置为 0 快:
tic;
length = 100000;
my_array(length) = 0;
for i = 1:length
my_array(i) = 5 * i;
end
toc;
这需要 0.0991 秒
现在显然这个微小的差异并不能证明太多,但你必须相信我,在一个包含许多优化的大文件中,差异变得更加明显。
为什么会这样?
预分配方法会分配一块内存供您使用。该内存是连续的并且可以预取,就像 C++ 或 Java 中的数组一样。但是,如果您不预先分配,那么 MATLAB 将不得不动态地找到越来越多的内存供您使用。据我了解,这与 Java ArrayList 的行为不同,更像是 LinkedList,其中数组的不同块被拆分到内存中的各个位置。
这不仅在您向其写入数据时会变慢(47 秒!),而且从那时起每次访问它时都会变慢。事实上,如果您绝对不能预先分配,那么在开始使用之前将矩阵复制到一个新的预先分配的矩阵中仍然很有用。
如果我不知道要分配多少空间怎么办?
这是一个常见问题,有几种不同的解决方案:
-
高估 - 严重高估矩阵的大小并分配太多空间比分配空间不足要好。
-
处理它并稍后修复 - 我经常看到开发人员忍受缓慢的填充时间,然后将矩阵复制到新的预分配空间中。通常将其保存为
.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;
这需要 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;
这需要 6 秒
这显然不是完美的预分配,之后翻转数组需要一点时间,但时间的改进不言自明。我希望有人有更好的方法来做到这一点,但同时这是一个很好的 hack。
数据结构
在内存使用方面,结构数组比数组结构差几个数量级:
% Array of Structs
a(1).a = 1;
a(1).b = 2;
a(2).a = 3;
a(2).b = 4;
使用 624 字节
% Struct of Arrays
a.a(1) = 1;
a.b(1) = 2;
a.a(2) = 3;
a.b(2) = 4;
使用 384 字节
如您所见,即使在这个简单/小示例中,结构数组使用的内存也比数组结构多得多。如果您想绘制数据,数组结构的格式也更有用。
每个结构都有一个大标题,正如您所见,一个结构数组会多次重复此标题,而数组结构只有一个标题,因此使用的空间更少。这种差异在更大的数组中更为明显。
文件读取
代码中freads(或任何与此相关的系统调用)的数量越少越好。
tic;
for i = 1:100
fread(fid, 1, '*int32');
end
toc;
前面的代码比下面的要慢很多:
tic;
fread(fid, 100, '*int32');
toc;
您可能认为这很明显,但同样的原则可以应用于更复杂的情况:
tic;
for i = 1:100
val1(i) = fread(fid, 1, '*float32');
val2(i) = fread(fid, 1, '*float32');
end
toc;
这个问题不再简单,因为在内存中浮点数是这样表示的:
val1 val2 val1 val2 etc.
但是,您可以使用 fread 的 skip 值来实现与以前相同的优化:
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;
所以这个文件读取是使用两个 freads 而不是 200 个完成的,这是一个巨大的改进。
函数调用
我最近编写了一些使用许多函数调用的代码,所有这些函数都位于单独的文件中。因此,假设有 100 个单独的文件,它们都相互调用。通过将这段代码“内联”到一个函数中,我发现执行速度从 9 秒提高了 20%。
显然,您不会以可重用性为代价这样做,但在我的例子中,这些函数是自动生成的,根本没有被重用。但我们仍然可以从中吸取教训,避免在不需要的地方调用过多的函数。
调用外部 MEX 函数会产生开销。因此,对大型 MEX 函数的一次调用比对较小 MEX 函数的多次调用效率高得多。
绘制许多断开的线
在绘制断开的数据(例如一组垂直线)时,在 Matlab 中执行此操作的传统方法是使用 hold on 迭代多次调用 line 或 plot。但是,如果您要绘制大量单独的线,这将变得非常缓慢。
我发现的技术使用了这样一个事实,即您可以将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);
我已经使用这种方法打印了数千条细线,并且性能得到了巨大的提升。不仅在初始绘图中,后续操作(例如缩放或平移操作)的性能也有所提高。