【问题标题】:Efficient Array Preallocation in MATLABMATLAB 中的高效数组预分配
【发布时间】:2013-08-19 00:47:58
【问题描述】:

在 MATLAB 中高效编程学习的第一件事是避免动态调整数组大小。标准示例如下。

N = 1000;

% Method 0: Bad
clear a
for i=1:N
    a(i) = cos(i);
end

% Method 1: Better
clear a; a = zeros(N,1);
for i=1:N
    a(i) = cos(i)
end

这里的“坏”变体需要 O(N^2) 时间来运行,因为它必须在循环的每次迭代中分配一个新数组并复制旧值。

我自己在调试时的首选做法是使用NaN 分配一个数组,这比0 更难与有效值混淆。

% Method 2: Easier to Debug
clear a; a = NaN(N,1);
for i=1:N
    a(i) = cos(i)
end

然而,有人会天真地认为,一旦我们的代码被调试,我们分配一个数组然后用0NaN 填充它是在浪费时间。如here 所述,您也许可以创建一个未初始化的数组,如下所示

% Method 3 : Even Better?
clear a; a(N,1) = 0;
for i=1:N
    a(i) = cos(i);
end

但是,在我自己的测试 (MATLAB R2013a) 中,我注意到方法 1 和 3 之间没有明显区别,而方法 2 需要更多时间。这表明 MATLAB 避免在调用 a = zeros(N,1) 时将数组显式初始化为零。

所以我很想知道

  • 在 MATLAB 中预分配(未初始化的)数组的最佳方法是什么? (最重要的是,大型数组)
  • 这在 Octave 中也成立吗?

【问题讨论】:

  • 这很有趣。我想也许 MATLAB 在修改矩阵之前没有初始化零(类似于 matlab 复制矩阵的方式),但在我的机器上,tic; a = NaN(1e4); a(1) = 1; toc 确实比tic; a = zeros(1e4); a(1) = 1; toc 慢。不过提醒一下,我真的只看到过使用zeros 完成的预分配,所以我很确定除非你要创建一个 mex 例程,否则没有办法在没有初始化的情况下进行预分配,但也许这里的其他人会知道.
  • 这正在迅速成为一个 Matlab 常见问题解答,并且该问题的各个方面已经在 SO 上进行了介绍。在其他地方也是如此,例如在非常宝贵的 Undocumented Matlab 博客上 --undocumentedmatlab.com/blog/allocation-performance-take-2/… 随着 Matlab 的发展,各种方法的比较速度似乎发生了变化。
  • @Shai 这是关于预分配方法的性能,而不是关于预分配的需要。请停止关闭这样的问题。
  • @EJG89 我觉得这些问题已经足够接近,足以证明“关闭为 dup”是合理的。我理解你不同意我的观点,这很好。对于此类分歧,可以选择重新投票。非常欢迎您投票支持重新开放。
  • 够近了吗?内存预分配的不同方式及其性能与内存预分配的需要有何不同。讨论处于更高级的水平,我只是不明白为什么你会如此草率地结束问题。我可能没有代表投票支持重新开放,所以你似乎只是在自己玩。您似乎只是不希望人们讨论 MatLab 的更深层/内部工作。我认为另一个主题甚至没有触及 JIT,而这是这个问题的基础。

标签: matlab memory-management octave


【解决方案1】:

让 Matlab 为您处理分配如何?

clear a;
for i=N:-1:1
    a(i) = cos(i);
end

然后,Matlab 可以用它认为最佳的任何东西(可能为零)分配和填充数组。不过,您没有NaNs 的调试优势。

【讨论】:

  • 你计时了吗?我认为它与方法(3)相同
  • 我没有计时,但我也希望如此(大多数情况下)。唯一可能的好处是您可以保证获得正确类的变量 - 分配 a(n) = 0 可能不会,然后可能需要重新分配。
【解决方案2】:

测试

使用 MatLab 2013b I 和 Intel Xeon 3.6GHz + 16GB RAM,我运行以下代码进行分析。我区分了 3 种方法,只考虑了一维数组,即向量。方法 1 和 2 已经使用列向量和行向量进行了测试,即 (n,1) 和 (1,n)。

方法 1(M1R、M1C)

a = zeros(1,n);

方法 2 M2R、M2C

a = NaN(1,n);

方法 3 (M3)

a(n) = 0;

结果

时序结果和元素个数已在图timing1D中以双对数刻度绘制。

如图所示,第三种方法的赋值几乎与向量大小无关,而另一种方法稳步增加,表明向量的隐式定义。

讨论

MatLab 使用 JIT(即时)进行大量代码优化,即在运行时进行代码优化。因此,提出代码运行速度更快的部分是由于编程(无论是否优化始终相同)还是由于优化是一个有效的问题。要测试此优化,可以使用 feature('accel','off') 关闭。再次运行代码的结果比较有趣:

表明现在方法 1 对行向量和列向量都是最优的。方法 3 的行为与第一个测试中的其他方法一样。

结论

优化内存预分配是无用的,而且浪费时间,因为 MatLab 无论如何都会为您优化。

请注意,内存应该是预先分配的,但分配的方式并不重要。预分配内存的性能很大程度上取决于 MatLab 的 JIT 编译器是否选择优化您的代码。这完全取决于 .m 文件的所有其他内容,因为编译器当时会考虑代码块然后尝试优化(它甚至有内存,因此多次运行文件可能会导致执行率更低 -时间)。此外,与之后执行的计算相比,考虑到性能,内存预分配通常是一个非常短的过程

在我看来,应该使用方法 1 或方法 2 预先分配内存,以维护可读的代码并使用 MatLab 帮助建议的功能,因为这些功能最有可能在未来得到改进。

使用的代码

clear all
clc
feature('accel','on')

number1D=30;

nn1D=2.^(1:number1D);

timings1D=zeros(5,number1D);

for ii=1:length(nn1D);
    n=nn1D(ii);
    % 1D
    tic
    a = zeros(1,n);
    a(randi(n,1))=1;
    timings1D(1,ii)=toc;
    fprintf('1D row vector method1 took: %f\n',timings1D(1,ii))
    clear a

    tic
    b = zeros(n,1);
    b(randi(n,1))=1;
    timings1D(2,ii)=toc;
    fprintf('1D column vector method1 took: %f\n',timings1D(2,ii))
    clear b

    tic
    c = NaN(1,n);
    c(randi(n,1))=1;
    timings1D(3,ii)=toc;
    fprintf('1D row vector method2 took: %f\n',timings1D(3,ii))
    clear c

    tic
    d = NaN(n,1);
    d(randi(n,1))=1;
    timings1D(4,ii)=toc;
    fprintf('1D row vector method2 took: %f\n',timings1D(4,ii))
    clear d

    tic
    e(n) = 0;
    e(randi(n,1))=1;
    timings1D(5,ii)=toc;
    fprintf('1D row vector method3 took: %f\n',timings1D(5,ii))
    clear e
end
logtimings1D = log10(timings1D);
lognn1D=log10(nn1D);
figure(1)
clf()
hold on
plot(lognn1D,logtimings1D(1,:),'-k','LineWidth',2)
plot(lognn1D,logtimings1D(2,:),'--k','LineWidth',2)
plot(lognn1D,logtimings1D(3,:),'-.k','LineWidth',2)
plot(lognn1D,logtimings1D(4,:),'-','Color',[0.6 0.6 0.6],'LineWidth',2)
plot(lognn1D,logtimings1D(5,:),'--','Color',[0.6 0.6 0.6],'LineWidth',2)
xlabel('Number of elements (log10[-])')
ylabel('Timing of each method (log10[s])')
legend('M1R','M1C','M2R','M2C','M3','Location','NW')
title({'Various methods of pre-allocation in 1D','nr. of elements vs timing'})
hold off

注意

包含c(randi(n,1))=1 的行;除了将值 1 分配给预分配数组中的随机元素之外,不要做任何事情,以便使用该数组来挑战 JIT 编译器。这些线不会显着影响预分配测量,即它们不可测量且不影响测试。

【讨论】:

  • 不错的测试,但a(randi(n,1))=1; 只初始化向量的一个随机元素,对吗?对我来说听起来很随意。我宁愿用2*i+1 之类的东西初始化每个元素,以防止一些可能的优化。
  • 该行旨在将 1 分配给随机元素,以便 JIT 优化不会检测到对数组没有执行任何操作。它不会初始化任何东西,而只会将 1 分配给预先分配的 0 或 nan 数组。
  • 我添加了一个注释来解释这些行的用途。
  • 如果 matlab 在内部为 M3 生成一个稀疏矩阵,并且只有在写入足够多的值时才分配完整的矩阵怎么办?设置一个随机值并不能阻止这一点。
  • MatLab 不会生成稀疏矩阵,因为我还映射了内存并且我看到它是预先分配的(即保留)并增加内存。随机值更像是一个标志:我确实使用了这个数组,不要完全忽略它。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2011-09-07
  • 1970-01-01
  • 1970-01-01
  • 2013-07-25
  • 2012-04-18
  • 2018-12-08
  • 1970-01-01
相关资源
最近更新 更多