关于重新组织代码效率的循环的问题

时间:2015-07-31 13:10:05

标签: matlab

这是问题

考虑以下功能:

function A = plodding(N,d)
for ii = 1:N
   jj = 1;
   A(ii,jj) = randn;
   while abs(A(ii,jj)) < d
      jj = jj + 1;
      A(ii,jj) = randn;
   end
end

重写此函数以消除减慢分配问题的速度。调用新功能,巡航。戴尔Latitude E6410具有7.8千兆字节的可用内存,消除了分配问题,因此产生了7倍的加速因子。

这是我的工作:

使用rng(0)的原始版本

function A = plodding(N,d)
rng(0); % To compare between the original and the modified version
for ii = 1:N
   jj = 1;
   A(ii,jj) = randn;
   while abs(A(ii,jj)) < d
      jj = jj + 1;
      A(ii,jj) = randn;
   end
end
end

修改后的版本

function A = cruising(N,d)
rng(0);
for jj = 1:N % Reorganize, so elems are added column-wise
   ii = 1;
   A(ii,jj) = randn;
   while abs(A(ii,jj)) < d
      ii = ii + 1;
      A(ii,jj) = randn;
   end
end
A = A'; % To get the matrix desired
end

但是当我测试时:

tic; A = plodding(5,4.5); toc
Elapsed time is 0.176091 seconds.

tic; A1 = cruising(5,4.5); toc;
Elapsed time is 39.285447 seconds.

B = A - A1; sum(B(:))
ans = 0

当然A = A1

根据我从课程中学到的东西,我的逻辑应该是正确的,因为MATLAB按列存储元素。有人可以帮帮我????

1 个答案:

答案 0 :(得分:0)

分配

这些差异很可能源于Matlab何时以及多久重新分配动态增长的矩阵。 请考虑以下输出:

>> clear();n = 1E5;A = rand(n,1);tic;for k = 2:500; A(:,k) = rand(n,1);end;toc;whos();
Elapsed time is 1.294744 seconds.
  Name           Size                 Bytes  Class     Attributes

  A         100000x500            400000000  double              
  k              1x1                      8  double              
  n              1x1                      8  double              

>> clear();n = 1E5;A = rand(1,n);tic;for k = 2:500; A(k,:) = rand(1,n);end;toc;whos();
Elapsed time is 31.106700 seconds.
  Name        Size                    Bytes  Class     Attributes

  A         500x100000            400000000  double              
  k           1x1                         8  double              
  n           1x1                         8  double

尽管将数组A增加到相同的大小,但逐列增长的速度快了近25倍。 由于Matlab是一种列主要语言,因此其数组的列在内存中始终是连续的。 因此,在第二个循环中,Matlab需要在每次迭代开始时将每个扩展列重新分配给连续的内存段,这可能会产生巨大的开销。 然而,在第一个循环中,Matlab可以在前一个循环之后立即寻址新列,如​​果空间存在,或者添加一个将前一列末尾连接到新列的指针,如果Matlab支持非连续数组(我不是&#39) ;我对Matlab内部知识一无所知,但我倾向于这种方法。)

函数plodding逐列(内部)然后逐行(外部)增长,而cruising逐行增长(内部),然后按列增长。 运行时间差异取决于这两种分配方法,但也取决于用于终止分配的条件(即阈值d)。


概率

接收小于或等于d的数字的概率是根据cumulative distribution functionnormal distribution计算得出的。 这可以使用统计工具箱normcdf计算,如果您没有工具箱,则可以使用以下匿名函数:

normcdf = @(x,mu,sigma) 0.5*(1 + erf((x-mu)./(sigma*sqrt(2))));

接收2或更小的概率为normcdf(2,0,1),为97.7%,接收值大于2的概率为1减去概率:1 - normcdf(2,0,1)或2.28%。 概率可用于估计数组中元素的数量,以获得大于d的数字。 例如,由于获得大于2的值的可能性为2.28%,因此平均大约两个2s的100个元素的数组很可能产生。

另一个例子,得到一个大于5的数字的概率是0.000029%,这意味着需要一个大约100万个元素的数组来平均得到5个。 请注意,我用于估计数组大小的公式是

elemApprox = 10^(abs(log10(1 - normcdf(5,0,1))))

然后我将它除以概率的有效数字(在这种情况下为2.8665),以获得对于得到一个5的元素的近似数量的粗略估计。 因为我们处理的是随机数,所以不会因为这样做而受到伤害。 为了更加确定,您可以将猜测乘以2或3或更多。


结果讨论

现在让我们讨论你得到的结果。从评论中,您提出了

>> tic;A = plodding(10000,2);toc
Elapsed time is 9.289355 seconds.
>> tic;A1 = cruising(10000,2);toc;
Elapsed time is 0.078783 seconds.

如上所述,阵列的尺寸为10,000乘约100(由于2的概率)。 然后,调用plodding(10000,2)将为其第一次迭代生成大约100列,然后按行逐行增长10,000次迭代。 如上所述,在Matlab中这是一个非常缓慢的过程,如果可能的话,最好避免使用。 然后,调用cruising(10000,2)将为其第一次迭代生成大约100行,然后逐列增长数组10,000次迭代。 如上所述,对于恒定行长,这是一个比行方式增长快得多的过程,但在某些情况下可能会非常慢。

您还发布了结果:

>> tic;A = plodding(5,5);toc
Elapsed time is 1.168961 seconds.
>> tic;A1 = cruising(5,5);toc;
% When I posted this thread, it's already more than 10 mins and MATLAB's still "busy"!

调用plodding(5,5)将为其第一次迭代生成大约1,000,000列,然后逐行增长数组5次迭代。 因此,在每次成功的内循环之后,Matlab必须将100万个左右的6个64位列重新分配到连续的内存插槽中。 虽然不一定快,但找到40个字节的百万个独立位置可能并不是世界上具有现代内存大小的最糟糕的事情。 调用cruising(5,5)将为其第一次迭代生成大约1,000,000行,然后逐列增长数组5次迭代。 这似乎比plodding调用更好,因为首先分配行然后添加列;但是,如果后续迭代所需的行数大于当前行数,则必须在每次内部迭代后重新分配数组的列,直到满足中断条件。

潜水更深一点,cruising(5,5)的第一次迭代(在我的计算机上)需要大约2.8秒才能运行并分配756,670行。 第二次迭代需要大约2.6秒来迭代分配的行,并且在等待超过该点60秒后,第二次迭代仅提前20,366行。 我会得出结论,重复添加行并使Matlab重新分配大约6MB连续的每列内存块会使函数变得非常慢。


建议

我对性能改进的建议是在进入循环之前分配A的行数。 对于plodding,这意味着在循环之前添加A(N,elemApprox) = 0;,对于cruising,这意味着在循环之前添加A(elemApprox,N) = 0;elemApprox是在不增长数组的情况下始终如一地达到条件所需的行/列的近似数量。

即使elemApprox早先将d = 5近似为1,000,000,但事实证明指定rng(0)的最大行数为3,151,807。 因此,对于elemApprox上的一致上限,我将上述近似公式修改为

elemApprox = 10^(ceil(abs(log10(1 - normcdf(5,0,1)))));

10,000,000。如果内存有限,这有点陡峭,但会大大增加运行时间。 使用分配公式,cruising的性能得到极大改善:

>> tic;plodding(5,5);toc;
Elapsed time is 0.576764 seconds.
>> tic;cruising(5,5);toc;
Elapsed time is 0.906496 seconds.

我不确定为什么plodding表现更好,但至少cruising现在在合理的时间内完成。

我还想指出,仅向A(N,1) = 0添加plodding并让Matlab知道会有多少行会导致小d的性能大幅增加:

>> tic;plodding(1E4,2);toc; %   without A(N,1) = 0;
Elapsed time is 18.479698 seconds.
>> tic;plodding(1E4,2);toc; %   WITH A(N,1) = 0;
Elapsed time is 0.052307 seconds.