MATLAB中嵌套的for循环(预分配)非常缓慢

4
我是一个有用的助手,可以为您翻译文本。

我正在尝试学习MATLAB,遇到的第一个问题之一是从具有静态摄像头和移动物体的图像序列中猜测背景。首先,我只想在时间上对像素进行平均或中位数,因此它只是要应用于4维数组的其中一行的单个函数

我已经将RGB图像加载到了具有以下维度的4维数组中:

uint8 [ num_images, width, height, RGB ]

这是我写的一个包含4个嵌套循环的函数。虽然我使用了预分配(preallocation),但仍然非常慢。我相信在C++中,这个函数至少可以运行快10倍到20倍,而且我认为在CUDA上它实际上可以实时运行。在MATLAB中,使用4个嵌套循环需要大约20秒的时间。我的堆栈(stack)有100张图像,尺寸为640x480x3。

function background = calc_background(stack)
tic;

si = size(stack,1);
sy = size(stack,2);
sx = size(stack,3);
sc = size(stack,4);

background = zeros(sy,sx,sc);
A = zeros(si,1);

for x = 1:sx
    for y = 1:sy
        for c = 1:sc
            for i = 1:si
                A(i) = stack(i,y,x,c);
            end
            background(y,x,c) = median(A);
        end
    end
end

background = uint8(background);

disp(toc);
end

你能告诉我如何让这段代码更快吗?我试过直接通过索引从数组中获取数据,速度非常快。用这种方法,代码只需要3秒钟就能完成,而不是20秒,所以性能提高了7倍,仅仅通过编写一个小函数。

function background = calc_background2(stack)
    tic;

    % bad code, confusing
    % background = uint8(squeeze(median(stack(:, 1:size(stack,2), 1:size(stack,3), 1:3 ))));

    % good code (credits: Laurent)
    background=uint8((squeeze(median(stack,1)));

    disp(toc);
end

现在我不明白为什么嵌套循环的版本如此缓慢,如果MATLAB可以这么快,那么为什么要这样做?我没有进行任何动态调整大小,MATLAB必须在内部运行相同的4个嵌套循环。为什么会发生这种情况?是否有方法使嵌套循环运行得更快,就像在C++中自然发生的那样?还是应该习惯使用一行代码的方式来编写MATLAB以获得最佳性能?更新:感谢所有伟大的答案,现在我理解了很多。我的原始代码stack(:, 1:size(stack,2), 1:size(stack,3), 1:3 ))毫无意义,与stack完全相同,我只是幸运地使用了median函数默认选项,将第一个维度用作其工作范围。我认为最好在另一个问题中询问如何编写高效的问题,因此我在这里提出了这个问题:如何在MATLAB中编写向量化函数

1
欢迎来到现实世界 ;) Matlab 在矩阵运算方面非常优化,但在循环方面却非常欺骗人心(如果情况反过来,它将被称为 Forlab)。 - Laurent'
1
欢迎使用MATLAB:具有FORTRAN的表现力和BASIC的速度。 - Derrick Turk
1
@zsero:除了循环问题,Matlab中的“median”函数速度较慢。建议改用fast_median - Jonas
3个回答

4
如果我理解你的问题,你是在问为什么 Matlab 在矩阵操作方面比过程式编程调用更快。答案很简单,那就是它就是这样设计的。如果你真的想知道是什么让它变得这样,你可以阅读Matlab网站上的这篇新闻通讯,其中讨论了一些底层技术,但你可能不会得到一个很好的答案,因为该软件是专有的。我还通过谷歌搜索找到了一些相关页面这个旧的SO问题似乎也能回答你的问题。

2
Matlab是一种解释性语言,这意味着它必须评估脚本中的每一行代码。
评估是一个漫长的过程,因为它必须解析、编译和解释每一行。使用带有简单操作的for循环意味着Matlab花费更多的时间来解析/编译而不是执行您的代码。
另一方面,内置函数是用编译语言编写并且经过了大量优化。它们非常快,因此速度差异很大。
底线:我们非常习惯于过程式语言和for循环,但几乎总有一种好的、快速的方法以向量化的方式完成相同的事情。
为了完整和表彰功勋,最近版本的Matlab实际上尝试通过分析重复操作来加速循环,将重复操作的块编译成本机可执行文件。这称为即时编译(JIT),由Jonas在以下评论中指出。
如果我理解正确(您想要第一维的中位数),您可以尝试以下方法:
background=uint8((squeeze(median(stack,1)));

这就是和我的答案一样的,只不过更整洁。我对了解为什么会发生这种情况以及是否有办法使嵌套循环像这样快速运行感兴趣。 - hyperknot
MATLAB也是过程化的(同时也是面向对象的),只是它与大多数其他编程语言有所不同,即以数组(或矩阵)为重点。MATLAB擅长对数组进行操作(大规模操作),但在较小的操作上(仅对单个元素进行操作的操作)则不太擅长。 - Egon
@Laurent:最近版本的Matlab使用即时编译器来加速代码,它已经非常擅长优化简单循环。不过,向量化更好,只要你能承受得起内存消耗。 - Jonas
好的,现在我正在学习更多。我应该学习的关键词是矢量化。尽管我认为我在这里只是幸运的,因为中位数已经实现了这种矢量化。但如果我需要编写自己的函数,我该怎么办呢?如何避免循环?(我认为最好将其作为一个单独的问题来问) - hyperknot

1

嗯,两者之间的区别在于它们执行代码的方法。简单来说,在C语言中,您将代码提供给编译器,编译器会尝试优化您的代码或将其转换为机器码。这需要一些时间,但当您实际执行程序时,它已经是机器码了,因此执行非常快。您的编译器可能需要很长时间来优化代码,但通常情况下,您不关心编译一个可分发程序需要花费1分钟还是10分钟。

MATLAB(和其他解释型语言)通常不是这样工作的。当您执行程序时,解释器将解释每行代码并即时将其转换为一系列机器码。如果您编写for循环,则速度会稍慢,因为它必须一遍又一遍地解释代码(至少原则上是这样,对于最新版本的MATLAB来说,可能有其他开销更重要的问题)。这里的难点在于所有事情都必须在运行时完成:解释器可以执行一些优化,但是执行耗时的优化可能会在某些情况下显著提高性能,但会导致大多数其他情况下性能下降。

您可能会问使用MATLAB有什么好处?您将获得灵活性和清晰的语义。当您想要进行矩阵乘法时,只需直接编写即可;在C中,这将产生一个双重for循环。您几乎不用担心数据类型、内存管理等问题...
在幕后,MATLAB使用已编译的代码(如果我没记错的话是Fortan/C/C++)来执行大型操作:因此,矩阵乘法实际上是由从另一种语言编译而成的机器码执行的。对于较小的操作,情况也是如此,但是您不会注意到这些计算的速度,因为大部分时间都花在管理代码上(传递变量、分配内存等)。
总之,您应该习惯这样的简洁陈述。如果您看到像Laurent示例中的代码行,您立即就会看出它计算了堆栈的中位数。您的代码需要11行代码才能表达相同的含义,因此当您查看像您的代码(可能嵌入了数百行其他代码)时,您将更难理解正在发生什么并确定某个操作发生的位置。
更进一步地说:你不应该像在C/C++中编程那样在MATLAB中编程;反之亦然。每种语言都有其优势和劣势,要学会了解它们并将每种语言用于其所擅长的领域。例如,你可以在MATLAB中编写整个编译器或Web服务器,但通常这将非常缓慢,因为MATLAB并不适用于处理或连接字符串(虽然它可以,但速度可能非常慢)。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接