Matlab:在循环中重复调用相同的mex函数会产生太多开销吗?

8

我有一些Matlab代码需要加速。通过分析,我确定了一个特定函数是导致执行减慢的罪魁祸首。在循环中调用该函数数十万次。

我的第一个想法是将该函数转换为mex(使用Matlab Coder),以加速它。但是,常识告诉我,在Matlab和mex代码之间的接口会带来一些开销,这意味着调用此mex函数数千次可能并不是一个好主意。这个理解正确吗?或者当重复调用相同的mex时,Matlab是否会施展魔法来消除开销?

如果有明显的开销,我想重新构建代码,将循环添加到函数本身中,然后创建该函数的mex。在这样做之前,我要验证我的假设,以证明花费时间的必要性。

更新:

我尝试了@angainor的建议,并创建了donothing.m文件,其中包含以下代码:

function nothing = donothing(dummy) %#codegen
nothing = dummy;
end

然后,我从这个函数创建了一个名为donothing_mex的mex函数,并尝试了以下代码:

tic;
for i=1:1000000
    donothing_mex(5);
end
toc;

结果表明,调用该函数一百万次大约需要9秒。对于我们的目的来说,这并不是一个重要的开销,因此我认为现在我只需要将被调用的函数转换为mex即可。然而,回想起来,从循环中调用一个函数,而这个循环执行了大约一百万次,似乎是一个相当愚蠢的想法,特别是考虑到这是关键性能代码,因此将循环移动到mex函数中仍然在计划之内,但优先级较低。


1
一定要将循环移动到代码中。这应该只需要2或3行额外的MEX代码,可以节省大约9秒钟中的8.5秒。 - twerdster
2
从你对@angainor答案的评论来看,你采取的方法有一种XY问题的余味,例如,你想创建的MEX为了解决性能问题可能在Matlab中有一个更快的解决方案,只是你之前没有想到过。你能否发布一下你现在想要执行的循环计算的本质? - Rody Oldenhuis
@RodyOldenhuis 这也是一个很好的观点。过早优化是万恶之源 ;) - angainor
@RodyOldenhuis 这基本上是根据这里给出的算法实现的:http://ginstrom.com/scribbles/2007/12/01/fuzzy-substring-matching-with-levenshtein-distance-in-python/ 与该代码唯一的区别是,我使用了一个名为'dist'的单矩阵来存储Levenshtein距离矩阵,而不是使用两个变量row1和row2。 - Sundar R
3个回答

5
通常情况下,一切都取决于您在MEX文件中所做的工作量。调用MEX函数的开销是恒定的,并不取决于问题大小等因素。这意味着参数不会复制到新的临时数组中。因此,如果工作量足够大,则调用MEX文件的MATLAB开销将不会显示出来。无论如何,在我的经验中,仅在第一次调用mex函数时MEX调用开销才显着——动态库必须加载,符号必须解析等等。随后的MEX调用几乎没有开销,并且非常高效。
由于MATLAB是高级语言,与其性质有关的开销几乎影响了MATLAB中的所有内容。除非您有一个代码,您确定它已完全使用JIT编译(但那时您不需要mex文件:)),所以你只能选择其中的一个开销。
总之,我不会对MEX调用开销过于担心。 编辑 如在这里和其他地方经常听到的,任何特定情况下唯一合理的做法当然是进行基准测试并自己检查。您可以通过编写一个微不足道的MEX函数轻松估算MEX调用开销:
#include "mex.h"
void mexFunction(int nlhs, mxArray *plhs[ ], int nrhs, const mxArray *prhs[ ]) 
{      
}

在我的电脑上,您可以获取到:
tic; for i=1:1000000; mexFun; end; toc
Elapsed time is 2.104849 seconds.

每次 MEX 调用的开销为 2e-6 秒。添加您的代码,计时并查看开销是否处于可接受的水平。

正如 Andrew Janke 在下面指出的(谢谢!),MEX 函数的开销显然取决于您传递给 MEX 函数的参数数量。这是一个小依赖关系,但它确实存在:

a = ones(1000,1);
tic; for i=1:1000000; mexFun(a); end; toc
Elapsed time is 2.41 seconds.

这与a的大小无关:

a = ones(1000000,1);
tic; for i=1:1000000; mexFun(a); end; toc
Elapsed time is 2.41805 seconds.

但它与参数的数量有关

a = ones(1000000,1);
b = ones(1000000,1);
tic; for i=1:1000000; mexFun(a, b); end; toc
Elapsed time is 2.690237 seconds.

因此,在您的测试中可能需要考虑这一点。


@sundar 我无法告诉你可以期望什么,因为我不知道你处理的代码和问题。如果你不想投入太多时间,为什么不编写一个微不足道的mex文件,它只是做加两个数字或返回 x+1,并测量开销呢?正如我所说,这是恒定的,但显然取决于你的硬件/操作系统/MATLAB版本。你将能够将其添加到你的C代码性能中,并查看在MATLAB+MEX中可以期望什么。 - angainor
1
请注意,相对于数组大小而言,开销将是恒定的,但可能会随着函数参数数量的增加而增加;原始数据与写时复制共享,但在我查看的版本中,mxarray数据结构的新“头”部分被构建并传递给函数。(这可能是Matlab为了防御而采取的措施,并可能解释一些较高的MEX文件开销。) - Andrew Janke
@AndrewJanke 很好的观点,我已经更新了我的答案。谢谢。还有,在Matlab中各种方法的开销方面,你做得很好。 - angainor
我进行了一些计算,发现这段代码大约需要运行3e11次迭代,因此理论上开销会累积数天!这是一个三层嵌套循环,其中最外层是parfor,所以我将内部两个循环移入代码中以进行mex化。 我还对函数本身的不同版本进行了基准测试:基本的Matlab版本、mex化版本、循环在mex代码内部的版本以及直接出现在循环内部而不是函数调用的版本。循环内部版本赢得了胜利,因此我将继续实施它。谢谢! - Sundar R
@sundar 很好,虽然自己用 C 实现会更好,但既然你不能做到,那么...也许你可以发布详细的基准测试结果 - 这将非常有用。 - angainor
显示剩余4条评论

2

好的,这是我在Matlab中能做到的最快速度:

%#eml
function L = test(s,t)

    m = numel(s);
    n = numel(t);

    % trivial cases
    if m==0 && n==0
        L = 0; return; end
    if n==0
        L = m; return; end
    if m==0
        L = n; return; end

    % non-trivial cases
    M = zeros(m+1,n+1);    
    M(:,1) = 0:m;

    for j = 2:n+1
        for i = 2:m+1
            M(i,j) = min([
                M(i-1,j) + 1
                M(i,j-1) + 1
                M(i-1,j-1) + (s(i-1)~=t(j-1));
                ]);
        end
    end

    L = min(M(end,:));

end

你能否编译并运行一些测试?(由于某种奇怪的原因,编译在我的安装中无法正常工作...)如果你认为这更容易,也许先将%#eml更改为%#codegen

注意:对于C版本,您还应该交换for循环,使j的循环成为内循环。

此外,row1row2方法更加节省内存。如果您要编译,请使用该方法。


这几乎是我的代码的完全复制(除了我们不需要处理微不足道的情况)。相对于%#codegen,%#eml是做什么的?关于for循环交换的建议很好,谢谢(我猜想由于C是行主序,交换会使缓存更友好,是这样吗?)至于row1、row2的方法,内存效率也意味着更快吗? - Sundar R
1
@sundar:%# eml 告诉Matlab使用嵌入式Matlab,这是Matlab可以直接编译成机器代码的子集。%#codegen用于生成C代码(我从未使用过它,因为它不在R2010b中:)。是的,C是行主序。这通常是在C系列和Fortran系列语言之间进行转换的一个重要障碍。 row1,row2方法:它可能只会快一点,它将只使用更少的内存,在比较较大的字符串时可能很重要。另外,一个建议:始终实现任何微不足道的情况!! 它们迟早会发生,有意还是无意。 - Rody Oldenhuis
@sundar:但是为了我的理解,你在循环中调用这个函数无数次?这就是你想做的吗?如果是这样的话,请尝试将整个代码直接复制粘贴到循环体中(当然要进行正确接口的调整)。这样,Matlab的JIT将编译整个循环,您可能会获得很多速度(JIT无法处理循环内非内置函数的调用,因此您将以“解释速度”运行)。 - Rody Oldenhuis
@sundar:或者,允许test接受单元格字符串,并在test内部迭代它们的内容(实际上是一种更清晰的方法 :) - Rody Oldenhuis
干得好,终于弄清楚这个问题了 :) 今天看到Sundars的第二个问题后,我开始对MATLAB的JIT和效率产生了疑虑。生成的C代码相当复杂和低效... JIT编译的就是这个吗? - angainor
显示剩余2条评论

2

毫不犹豫地将循环移至mex文件中。

下面的示例演示了将一个几乎为空的工作单元放入for循环中,可以获得1000倍的加速。当然,随着for循环中工作量的增加,这种加速将会减少。

以下是差异的示例:

没有内部循环的Mex函数:

#include "mex.h"
void mexFunction(int nlhs, mxArray *plhs[ ], int nrhs, const mxArray *prhs[ ]) 
{      
    int i=1;    
    plhs[0] = mxCreateDoubleScalar(i);
}

在Matlab中调用:

tic;for i=1:1000000;donothing();end;toc
Elapsed time is 3.683634 seconds.

带有内部循环的Mex函数:

#include "mex.h"
void mexFunction(int nlhs, mxArray *plhs[ ], int nrhs, const mxArray *prhs[ ]) 
{      
    int M = mxGetScalar(prhs[0]);
    plhs[0] = mxCreateNumericMatrix(M, 1, mxDOUBLE_CLASS, mxREAL);
    double* mymat = mxGetPr(plhs[0]);
    for (int i=0; i< M; i++)
        mymat[i] = M-i;
}

在Matlab中调用:

tic; a = donothing(1000000); toc
Elapsed time is 0.003350 seconds.

1
当然,你是正确的,在这种情况下,你可以获得更快的代码,但问题不同。它涉及到调用MEX函数的开销。如果在MEX文件中进行的工作不仅仅是创建标量,那么你的加速可能会完全消失。这是一个明显的权衡,你的答案只适用于你特殊的场景。如果对mex文件的一次调用需要0.01秒,那么你根本不关心循环在哪里。而且,将任何循环移入MEX文件可能并不是微不足道的。 - angainor
当然,如果您在mex文件中执行更多的工作,那么您的加速可能会消失,但这与问题无关。该问题涉及到mex文件开销。我证明了,如果您在Matlab中循环调用1e6次mex文件,而不是在mex文件本身的循环内调用相同的功能,您将遭受mex文件调用开销。我不确定您为什么会有其他想法。 - twerdster
我不会对此进行争论。我已经给出了绝对的开销估计。OP明确表示,在他决定将循环移入MEX文件之前,他想确定这是否值得努力。根据您的说法,这总是值得的吗?因为我认为这取决于1)他在循环的1次迭代中所做的工作量和2)将循环结构移动到MEX函数所需的编程工作量。事实并非总是只需要2行代码就能完成。在您的示例中可能只需要2行代码。 - angainor
1
仅供完整性参考:您看到的差异主要是由于Matlab的JIT编译器无法编译循环,因为它包含非内置的mex函数。如果您执行 tic;for i=1:1000000;i=1;end;toc(即相同的功能,但可以进行JIT编译),在我的机器上只需要0.005825秒。这正是为什么循环应该始终转移到mex中的原因。 - Rody Oldenhuis

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