【问题标题】:Is it efficient to use length() in loops?在循环中使用 length() 是否有效?
【发布时间】:2014-07-03 20:51:33
【问题描述】:

我在 MATLAB 中迭代向量时遇到很多情况,

所以我这样做了:

len = length(vector)
for i=1:len
 do_something();
end

但这只是一种直觉,说“防止每次都调用length() 函数”。这是真的吗?还是在时间要求方面与此相同:

for i=1:length(vector)
 do_something();
end

感谢您的帮助!

【问题讨论】:

  • 你可以量一下...
  • 可以,但我也想了解一下MATLAB编译代码的方式。
  • 如果您测量它,请尝试numel。可能比length更快
  • 如果您进行一个简单的测试并比较上述建议的运行时间,将非常感激,您可以使用 matlab 中的 'tic toc' 函数。然后向我们展示结果:)。我也会选择numel
  • 我怀疑它每次都会调用它,我很确定解释器会首先创建一个 1:length(vector) 的向量,然后仅基于该向量开始迭代过程。我无法想象您之前会看到任何性能差异调用长度。这不像您在循环本身内部调用它,这可能会有所不同。

标签: performance matlab loops time-complexity variable-length


【解决方案1】:

它们是一样的。在 Matlab 的特定情况下,不要担心:将length() 或任何其他函数放在for 初始化子句中总是与在循环外评估它一样快,因为for 只会调用它一次无论哪种方式。您的直觉可能是基于其他一些语言的 for 循环,例如 C 和 Java,它们具有不同的行为。

根据定义,Matlab for 循环仅在循环开始时对其参数表达式求值一次,以预先计算循环索引变量 (i) 的值范围或数组以在内部执行循环通过。与许多其他语言不同,Matlab 的for 不会在每次循环时重新评估某些循环控制语句。 (这也是为什么在 Matlab for 循环的主体内分配循环索引变量没有效果,而在 C 或 Java 中,它将允许您“跳来跳去”并改变控制流。)

阅读 Matlab for documentation。它可能会更明确,但您会注意到它是根据表达式解析为的值定义的,而不是表达式本身。

语法等价

一个 C for 循环被定义为具有此行为。

/* C-style for loop */
for ( A; B; C; ) {
    ...
}

/* basically equivalent to: */
{
    A;
    while ( B ) {
        ....
        C;
    }
}

在功能上,Matlab 的for 循环语法等价性更像这样。

% Matlab for loop

for i = A:B
    ...
end

% basically functionally equivalent to:

tmp_X = A:B;         % A and B only get evaluated outside the loop!
tmp_i = 1;           % tmp_* = an implicit variable you have no access to
while tmp_i < size(tmp_X,2)
    i = tmp_X(:,tmp_i);
    ...
    tmp_i = tmp_i + 1;
end

在实践中,Matlab 可以在原始值的情况下优化具体数组tmp_X 的创建。循环体与控制表达式的这种分离也有助于支持并行计算工具箱中使用的并行parfor 循环,因为每次循环迭代的循环索引变量的值在循环开始之前是已知的,并且与执行无关任何循环通过。

演示

您可以通过在循环控制子句中使用具有可观察到的副作用的函数来自己确认此行为。

function show_for_behavior

for i = 1:three(NaN)
    disp(i);
end

function out = three(x)
disp('three() got called');
out = 3;

您可以看到整个循环只有一次调用。

>> show_for_behavior
three() got called
     1
     2
     3

语言原因

这是我推测的地方。

除了方便之外,我怀疑 Matlab 以它的方式定义其 for 循环的原因之一,而不是在常规 while 循环上为您提供 C 风格的语法糖,是因为获取索引变量很棘手由于浮点舍入。默认情况下,您使用的数字循环变量是双精度数,对于较大的 x(大约 10^15)值,x + 1 == x,因为 x (eps(x)) 处的相对精度大于 1。

因此,如果您像这样对for i = A:B ... end 进行天真的while-loop 转换,您将有一个无限循环,因为在每一步,i = i + 1 将导致i 的相同值,因为四舍五入。

i = A;
while (i < B)
    ...
    i = i + 1;
end

为了能够对大值序列执行循环,您可以计算值范围和步数,使用单独的整数值跟踪循环索引,并使用该整数值为每个步骤构造 i 值计数器和步长,而不是在每次通过时增加一个临时变量。像这样的。

% original
for x = A:S:B; ...; end

% equivalent
nSteps = int64( ((B - A) / S) ) + int64(1);
i = int64(0);
while i < nSteps
    x = A + (S * double(i));
    ....
    i = i + int64(1);
end

只有在所有通道的最小值、最大值和步长范围都提前定义的情况下,您才能执行此操作,更灵活的while-loop 形式无法保证这一点。

请注意,在这种情况下,对于较大的 A 和 B,x 可能在多次迭代中具有完全相同的值,但最终会继续进行,并且如果您使用无限精度值而不是近似浮点值。我怀疑这与 Matlab 在这些情况下内部所做的有关。

这是一个显示此行为的示例。

function big_for_loop(a)

if nargin < 1;  a = 1e20;  end
b = a + 4 * eps(a);
step = 15;

fprintf('b - a = %f\n', b - a);
fprintf('1 + (b - a) / step = %f\n', 1 + (b - a) / step);

last_i = a;
n = 0;
for i = a : step : b
    n = n + 1;
    if (i ~= last_i); disp('i advanced');  end
    last_i = i;
end
fprintf('niters = %d\n', n);

当我运行它时,我会根据 eps 进行更改,如果这就是 Matlab 执行循环的方式。

>> big_for_loop
b - a = 65536.000000
1 + (b - a) / step = 4370.066667
i advanced
i advanced
i advanced
i advanced
niters = 4370

【讨论】:

    【解决方案2】:

    如果您担心性能或速度 - 真的不用担心。它不会产生任何明显的差异。 @David 在他的回答中给出了一些时间来支持这一点。

    在美学方面,我通常会写出它的价值

    for i = 1:numel(v)
        doSomething(v(i))
    end
    

    我更喜欢numel 而不是length,因为length 为您提供最长数组维度的长度,而不是第一个维度的长度。如果您始终确定它是一个向量,它们是相同的,但我会尽量避免这种假设。

    当然,如果我需要单独访问numel(v),那么值得将其提取到中间变量vNum 并写入for i = 1:vNum

    如果您对 for 循环索引的语义感兴趣,它比您想象的要复杂。当您编写 for i = 1:N 时,MATLAB 并不总是简单地创建向量 1:N 然后遍历它。例如,你可以写

    for i = 1:realmax
        i
    end
    

    for i = 1:Inf
        i
    end
    

    甚至

    for i = Inf:-1:1
        i
    end
    

    它会起作用(按 Ctrl-C 转义)。在这些情况下,MATLAB 不会将循环索引创建为向量,它也不能,因为它太大了。

    解析器具有比这更复杂的逻辑,具有多种边缘情况和优化的知识。有时它会在循环之前创建循环索引,有时在运行中。我保证,除非您可以访问 MATLAB 源代码,否则您将无法猜测所有内部结构。

    还要注意,循环索引不必是向量;如果您提供一个数组,MATLAB 会遍历这些列。例如,

    for i = magic(3)
        i
    end
    

    依次显示数组的列。同样,您可以提供一个单元格数组,它将遍历单元格(注意,索引是单元格,而不是单元格内的元素)。

    出于这个原因,我有时会写

    for vElement = v
        doSomething(vElement)
    end
    

    而不是上面的第一个模式使用numel

    当然,根据应用程序,矢量化doSomething 并调用doSomething(v) 可能会更好。

    最后一件事——到目前为止,我们真正讨论的是整数索引,但请记住,冒号运算符可以有任何增量,例如pi:sqrt(2):10;请记住,冒号运算符与linspace 不同,并且不会给您相同的答案。

    【讨论】:

    • size 与其他两个相比如何?
    • @excaza 对于内置类型的数组,numel(x)prod(size(x)) 相同。对于用户自定义类的数组,也是这样,除非你重载了numel
    • 哦,呵呵。我已经习惯了在特定维度上使用numel,以至于我忘记了它实际返回的内容。哎呀。
    • @SamRoberts:一个警告:对于用户定义的类,numel(x)(和length(x))将忽略用户定义的size(x) 方法,所以如果你重载一个,你实际上必须重载所有与大小和形状相关的函数以获得一致的行为。 (即numel(x) 更像prod(builtin('size', x))。)
    • +1,很高兴看到numellength 更受欢迎。我在实践中遇到了一堆错误,其中length 被用于最终通过非向量输入的东西。
    【解决方案3】:

    我运行了一个测试,首先使用 JIT 编译(我每个测试运行了 3 次)

    length(vector) 12.86 12.50 12.44    
    N              13.00 12.52 12.55   
    numel(vector)  12.83 12.55 12.56
    

    在关闭 JIT 编译的情况下,

    length(vector) 13.12 13.43 12.95   
    N              12.54 13.04 13.00   
    numel(vector)  12.57 12.92 12.72
    

    我的结论是,在启用 JIT 编译的情况下,你做什么都没有关系,但是如果没有 JIT,那么 numelN 是最好的,但它似乎并没有太大的区别。我无法解释这一点,也许这个测试有缺陷,但是你去吧。

    代码如下:

    clear
    N=5e4;
    A=rand(1e2);
    tic()
    vector=ones(1,N);
    for i=1:length(vector)
        inv(A);
    end
    toc()
    tic()
    for i=1:N
        inv(A);
    end
    toc()
    tic()
    vector=ones(1,N);
    for i=1:numel(vector)
        inv(A);
    end
    toc()
    

    【讨论】:

    • 一种可能的解释:numellength 更“基本”,因为numel 直接测量数组使用的总内存量;而length 必须确定内存是如何组织成不同维度的,找到每个维度的“范围”,然后给出这些“范围”的最大值。
    • 有相当多的可变性,所以我不确定结果的结论性如何。这可能只是一个糟糕的测试。
    • 嗯,计算逆需要的时间比你想要测量的要多得多。所以我建议用一个非常简单的操作来替换那条线。这样差异(如果有的话)会更加突出
    • 除了 Luis Mendo 所说的之外,我还会运行三倍以上的测试(所有结果的平均值)。很少有试验受环境影响(CPU 使用率、其他程序、磁盘 I/O、Windows 更新、tic/toc 分辨率和准确性......)
    • @LuisMendo numel 就像prod(size(x)) 一样实现,除非您为自己的类重载了它。它不是通过用于数组的内存计算的。 length 也使用size,但返回数组的最长维度(基本上是max(size(a)),除非数组为空,在这种情况下它始终为零)。当然,它们都是内置的,不会直接调用那些 MATLAB 函数。
    猜你喜欢
    • 2011-07-05
    • 2020-07-29
    • 1970-01-01
    • 1970-01-01
    • 2021-02-28
    • 1970-01-01
    • 1970-01-01
    • 2021-09-30
    • 1970-01-01
    相关资源
    最近更新 更多