【问题标题】:performance difference between subscript indexing and linear indexing下标索引和线性索引之间的性能差异
【发布时间】:2016-04-16 11:36:19
【问题描述】:

我在 MATLAB 中有一个二维矩阵,我使用两种不同的方式来访问它的元素。一种基于下标索引,另一种基于线性索引。我通过以下代码测试这两种方法:

N = 512; it = 400; im = zeros(N);
%// linear indexing
[ind_x,ind_y] = ndgrid(1:2:N,1:2:N);
index = sub2ind(size(im),ind_x,ind_y);

tic
for i=1:it
    im(index) = im(index) + 1;
end
toc %//cost 0.45 seconds on my machine (MATLAB2015b, Thinkpad T410)

%// subscript indexing
x = 1:2:N;
y = 1:2:N;

tic
for i=1:it
    im(x,y) = im(x,y) +1;
end
toc %// cost 0.12 seconds on my machine(MATLAB2015b, Thinkpad T410)

%//someone pointed that double or uint32 might an issue, so we turn both into uint32

%//uint32 for linear indexing
index = uint32(index);
tic
for i=1:it
    im(index) = im(index) +1;
end
toc%// cost 0.25 seconds on my machine(MATLAB2015b, Thinkpad T410)

%//uint32 for the subscript indexing
x = uint32(1:2:N);
y = uint32(1:2:N);
tic
for i=1:it
    im(x,y) = im(x,y) +1;
end
toc%// cost 0.11 seconds on my machine(MATLAB2015b, Thinkpad T410)

%% /*********************comparison with others*****************/
%//third way of indexing, loops
tic
for i=1:it
    for j=1:2:N
        for k=1:2:N
            im(j,k) = im(j,k)+1;
        end
    end
end
toc%// cost 0.74 seconds on my machine(MATLAB2015b, Thinkpad T410)

看来直接使用下标索引比从sub2ind得到的线性索引要快。有谁知道为什么?我以为它们几乎是一样的。

【问题讨论】:

  • sub2ind 有一些开销。试试它的“手动”版本index = bsxfun(@plus, (1:2:size(im,1)).', ((1:2:size(im,2))-1)*size(im,1))。另外,最好使用timeit 而不是tic/toc
  • 哎呀,现在我看到我提到的开销超出了你的时间
  • @Luis,我没有计算 sub2ind 的时间。只有两种访问矩阵中元素的方法。
  • @TroyHaskin 据我了解,在 MATLAB 中,下标索引是语法糖。 MATLAB 在内部将所有高维矩阵存储为线性数组 (mathworks.com/help/matlab/matlab_prog/…)
  • @Dan,是的,我认为 Matlab 具有线性存储。这就是为什么这段代码让我感到困惑。在线性情况下,线性索引应该比下标方式更快。但结果却恰恰相反。

标签: matlab indexing


【解决方案1】:

直觉

正如 Daniel 在他的 answer 中提到的,线性索引在 RAM 中占用更多空间,而下标要小得多。

对于下标索引,在内部,Matlab 不会创建线性索引,但会使用(双)编译循环循环遍历所有元素。

另一方面,下标版本必须循环遍历所有从外部传递的线性索引,这将需要从内存中读取更多,因此需要更长的时间。

索赔

  • 线性索引更快
  • ...只要索引总数相同

时间

从时间安排中,我们看到了第一个声明的直接确认,我们可以通过一些额外的测试推断出第二个声明(如下)。

LOOPED
      subs assignment: 0.2878s
    linear assignment: 0.0812s

VECTORIZED
      subs assignment: 0.0302s
    linear assignment: 0.0862s

第一个声明

我们可以用循环来测试它。 subref 操作的数量相同,但线性索引直接指向感兴趣的元素,而在内部需要转换下标。

感兴趣的函数:

function B = subscriptedIndexing(A,row,col)
n = numel(row);
B = zeros(n);
for r = 1:n
    for c = 1:n
        B(r,c) = A(row(r),col(c));
    end
end
end

function B = linearIndexing(A,index)
B = zeros(size(index));
for ii = 1:numel(index)
    B(ii) = A(index(ii));
end
end

第二次索赔

此声明是从使用矢量化方法时观察到的速度差异推断出来的。

首先,矢量化方法(相对于循环)加快了下标赋值,而线性索引稍慢(可能没有统计学意义)。

其次,两种索引方法的唯一区别在于索引/下标的大小。我们希望将其隔离为导致时间差异的唯一可能原因。另一个主要参与者可能是 JIT 优化。

测试功能:

function B = subscriptedIndexingVect(A,row,col)
n = numel(row);
B = zeros(n);
B = A(row,col);
end

function B = linearIndexingVect(A,index)
B = zeros(size(index));
B = A(index);
end

注意:我保留了B 的多余预分配,以保持矢量化和循环方法的可比性。换句话说,时间上的差异应该只来自索引和循环的内部实现。

所有测试都运行:

function testFun(N)
A             = magic(N);
row           = 1:2:N;
col           = 1:2:N;
[ind_x,ind_y] = ndgrid(row,col);
index         = sub2ind(size(A),ind_x,ind_y);

% isequal(linearIndexing(A,index), subscriptedIndexing(A,row,col))
% isequal(linearIndexingVect(A,index), subscriptedIndexingVect(A,row,col))

fprintf('<strong>LOOPED</strong>\n')
fprintf('      subs assignment: %.4fs\n',  timeit(@()subscriptedIndexing(A,row,col)))
fprintf('    linear assignment: %.4fs\n\n',timeit(@()linearIndexing(A,index)))
fprintf('<strong>VECTORIZED</strong>\n')
fprintf('      subs assignment: %.4fs\n',  timeit(@()subscriptedIndexingVect(A,row,col)))
fprintf('    linear assignment: %.4fs\n',  timeit(@()linearIndexingVect(A,index)))
end

开启/关闭 JIT 有没有影响:

feature accel off
testFun(5e3)
...

VECTORIZED
      subs assignment: 0.0303s
    linear assignment: 0.0873s

feature accel on
testFun(5e3)
...

VECTORIZED
      subs assignment: 0.0303s
    linear assignment: 0.0871s

排除下标赋值的卓越速度来自 JIT 优化,这给我们留下了唯一可能的原因,RAM 访问次数。确实,最终矩阵具有相同数量的元素。但是,线性赋值必须检索索引的所有元素才能获取数字。

设置

使用 MATLAB R2015b 在 Win7 64 上测试。由于最近Matlab's execution engine 的变化,Matlab 的早期版本将提供不同的结果

事实上,在 Matlab R2014a 中关闭 JIT 会影响时序,但仅适用于循环(预期结果):

feature accel off
testFun(5e3)

LOOPED
      subs assignment: 7.8915s
    linear assignment: 6.4418s

VECTORIZED
      subs assignment: 0.0295s
    linear assignment: 0.0878s

这再次证实了线性分配和 sibscripted 分配之间的时序差异应该来自 RAM 访问的次数,因为 JIT 在矢量化方法中不起作用。

【讨论】:

    【解决方案2】:

    免责声明:我目前没有 MATLAB 许可证,因此我在下面提供的代码无疑未经测试。但是,如果有人决定测试,请相应地对此答案发表评论。

    根据您的 MATLAB 版本(您使用的是 R2015b 吗?),您可能在调用“零”时未支付预分配的全部前期费用。您可能会在 im 的第一次 get/set 时为分配付费,这会在您第一次访问 im 中的值时造成额外但隐藏的开销。

    见:http://undocumentedmatlab.com/blog/preallocation-performance

    作为初始测试,我建议您切换分析代码的顺序:

    N = 512; it = 400; im = zeros(N);
    
    %// subscript indexing
    x = 1:2:N;
    y = 1:2:N;
    
    tic
    for i=1:it
        im(x,y) = im(x,y) +1;
    end
    toc %// What's the cost now?
    
    %// linear indexing
    [ind_x,ind_y] = ndgrid(1:2:N,1:2:N);
    index = sub2ind(size(im),ind_x,ind_y);
    
    tic
    for i=1:it
        im(index) = im(index) + 1;
    end
    toc %// What's the cost now?
    

    为了更公平地描述下标与线性索引,我建议使用两种可能的方法之一:

    1. 通过创建两个单独的 im 矩阵 im1im2(最初均设置为零 (N))并使用每个矩阵,确保您在这两种方法上产生分配成本用于单独的索引方法。
    2. 在实际分析下标与线性索引之前,对 im 的每个元素运行完整的 get/set。

    方法一:

    N = 512; it = 400; im1 = zeros(N); im2 = zeros(N);
    
    %// subscript indexing
    x = 1:2:N;
    y = 1:2:N;
    
    tic
    for i=1:it
        im1(x,y) = im1(x,y) + 1;
    end
    toc %// What's the cost now?
    
    %// linear indexing
    [ind_x,ind_y] = ndgrid(1:2:N,1:2:N);
    index = sub2ind(size(im2),ind_x,ind_y);
    
    tic
    for i=1:it
        im2(index) = im2(index) + 1;
    end
    toc %// What's the cost now?
    

    方法二:

    N = 512; it = 400; im = zeros(N);
    
    %// Run a full get/set on each element to force allocation
    tic
    for i=1:N^2
        im(i) = im(i) +1;
    end
    toc 
    
    
    %// subscript indexing
    x = 1:2:N;
    y = 1:2:N;
    
    tic
    for i=1:it
        im(x,y) = im(x,y) +1;
    end
    toc %// What's the cost now?
    
    %// linear indexing
    [ind_x,ind_y] = ndgrid(1:2:N,1:2:N);
    index = sub2ind(size(im),ind_x,ind_y);
    
    tic
    for i=1:it
        im(index) = im(index) + 1;
    end
    toc %// What's the cost now?
    

    我有第二个假设,即当您显式声明要访问的每个元素时,会产生一些额外的开销,而不是让 MATLAB 为您推断元素。 excasa 的"duplicate post" reference(在我看来并不完全是重复的)具有相同的一般见解,但使用不同的数据点得出这个结论。我不会在这里写这个例子,但基本上,与较小的下标索引 xy 相比,创建一个直接向上的巨型数组 index给 MATLAB 更少的内部优化空间。我不知道 MATLAB 内部会执行这些特定优化,但也许它们来自您可能知道的黑魔法 MATLAB's JIT/LXE。如果您真的想检查 JIT 是否是这里的罪魁祸首(并且在 2014b 或更早版本中工作),那么您可以尝试禁用它,然后运行上面的代码。

    有几种方法可以禁用 JIT:

    1. 使用undocumented feature methods
    2. 将命令复制/粘贴到命令提示符,而不是直接从脚本编辑器运行。

    不幸的是,我不知道在 R2015a 及更高版本中关闭 LXE 的方法,并且尝试诊断 LXE 是否是罪魁祸首可能是一场艰苦的战斗。如果这是你被困的地方,也许你可以通过MathWorks' technical supportMathWorks Central 进一步研究。您可能会惊讶地发现来自这两种来源的一些令人惊叹的专家。

    【讨论】:

    • 运行所有代码 sn-ps,下标索引仍然比线性索引快得多(在所有情况下)。另外,我尝试使用 for 循环进行此操作,因此每次都使用标量进行索引,并且下标索引仍然更快。
    • 嗨,Dan,感谢您测试 sn-ps。出于好奇,您是否也尝试过禁用/重新启用 JIT?
    • 我没有尝试禁用 JIT
    【解决方案3】:

    下标索引在这里要快得多,这并不让我感到惊讶。如果您查看输入数据,在这种情况下索引要小得多。对于下标索引情况,您有 512 个元素,而对于线性索引情况,您有 65536 个元素。

    当您将示例应用于向量时,您会注意到两种方法之间没有区别。

    这是我用来评估不同矩阵大小的稍微修改的代码:

    it = 400; im = zeros(512*512,1);
    x = 1:2:size(im,1);
    y = 1:2:size(im,2);
    %// linear indexing
    [ind_x,ind_y] = ndgrid(x,y);
    index = sub2ind(size(im),ind_x,ind_y);
    
    tic
    for i=1:it
        im(index) = im(index) + 1;
    end
    toc 
    
    %// subscript indexing
    
    
    tic
    for i=1:it
        im(x,y) = im(x,y) +1;
    end
    toc 
    

    【讨论】:

    • 你对向量意味着什么?我用嵌套循环尝试了同样的事情并获得了相同的性能特征?
    • @Dan:我添加了代码。因为它创建了一个向量。将zeros(512*512,1) 更改为zeros(512*512,3),您会注意到线性索引版本在时间和索引大小上大致翻了一番,而下标索引版本只是稍微慢了一点。
    • @Daniel,这两种时尚应该访问相同数量的元素。不?否则,它们在功能上是不同的。那么,比较这两者是没有意义的。在我的代码中,它们执行完全相同的工作并访问相同数量的元素,但运行时间不同。
    • 它们访问的元素数量相同,索引区域的大小相同,但索引本身的大小不同。
    • @Daniel 但这是否适用于我的(现已被版主删除)示例,使用 for 循环并一次访问单个元素?在这种情况下,索引区域的大小永远不会大于 1 个元素。
    【解决方案4】:

    一个很好的问题。就在前面,我不知道正确的答案,但是,您可以分析行为。将第一个 toc 保存到 t1 中,将第二个 toc 保存到 t2 中。最后计算t1/t2。您将认识到,更改迭代次数或矩阵的大小(几乎)不会改变因子。 我建议:

    • 迭代次数只会提高 tictoc 的质量。 (很明显?)
    • 矩阵的大小没有影响,即语法中必须有时间延迟。

    我想,只是从线性索引到下标索引的内部检查或转换,即您执行的内部加法(操作)完全相同。使用下标索引而不是线性索引似乎更自然,所以mathworks 可能只是优化了第一个。

    更新: 您也可以简单地访问矩阵中的元素,您将看到,使用下标索引比使用线性索引更快。这支持了从线性到下标在内部完成 slow 转换的理论。

    【讨论】:

    • 我添加了第三种索引方式,使用下标直接访问矩阵中的一个元素。它是三种方法中最慢的。我猜它没有使用matlab中的向量化,所以它很慢。
    猜你喜欢
    • 1970-01-01
    • 2016-09-05
    • 2012-05-12
    • 1970-01-01
    • 1970-01-01
    • 2012-09-06
    • 1970-01-01
    • 1970-01-01
    • 2011-03-11
    相关资源
    最近更新 更多