MATLAB中预分配数组的替代方案是什么?

4

可能重复:
MATLAB中可扩展数据结构

在我的当前 MATLAB 脚本中,我有一个非常大的不确定大小的增长数组。目前我无法做任何事情,因为如果我实际上进行预分配,它将需要比应该需要的多得多的内存(每个像素的最大可能值是 640,但通常在 2-5 左右)。

通常在这种情况下,我会使用 C++ 中的向量或类似的东西,它们与给定容量呈指数关系增长。但是我认为 Matlab 中的矩阵开始更快地分段,比专门针对目的的 C++ 向量更快。

你们认为什么是这种情况下最好的替代方案?或者我应该坚持使用普通数组,并希望顺序添加约 100k 元素可以正常工作?

提前致谢。


可能重复:https://dev59.com/qU3Sa4cB1Zd3GeqPsiKi,https://dev59.com/BnI_5IYBdhLWcg3wDOnW - Amro
我搜索了一下,但是应该使用"growable"而不是"resizeable"和"preallocated"作为关键字。对此很抱歉。 - Xzhsh
4个回答

10

增长数组通常是一件不好的事情,至少如果你要多次这样做的话。有一些技巧可以使用。(我已经尝试过它们,并编写了工具,如果您想要的话,可以自动执行它们。)

问题在于与数组增长相关的时间是一个O(N^2)操作。它还倾向于使您的内存碎片化,如果您需要稍后创建一个大数组,则可能会导致潜在问题。

一种方法是将数组预先分配到某个固定大小,然后在超出数组大小时附加大块零。现在使用矩阵索引将新值插入现有数组中。思路是每当需要重新分配数组时,将数组的大小加倍。这只会导致少量的重新分配,但最终可能会附加许多不需要填充的零。最后,您可以丢弃多余和未使用的数组元素。

第二个技巧是使用单元数组。每次想要添加新元素或块时,只需附加一个带有这些元素的新单元格。然后在最后,进行cell2mat操作。单元格技巧的问题在于它仍然是O(N^2)操作,因为单元格数组本身实质上是指针数组,当您附加新元素时,该指针列表必须被重新分配。
有一种组合技巧可以做到我喜欢的,但需要工具来实现操作。这个想法是创建包含10000个元素块的单元格。用零填充第一个块。然后使用矩阵索引插入生成的新元素。矩阵索引当然很快。当您在该单元格中用完空间时,附加一个新的大单元格。然后在最后,将所有单元格连接在一起,小心地删除未使用的元素。
这个最后的方案涉及相对较少的新单元格元素要附加,因此如果可能有数百万个附加步骤,则整体效率最高。

你可以在我多年来发布的几个文件交换提交中找到这些方法的具体实现。第一个可以在grow_array中找到,它内部管理附加步骤,为您担心矩阵索引。

后来,我使用函数句柄或持久变量构建了最后一种方案来维护存储的信息。包含这些实现的工具可以在growdata和growdata2中找到。


6

当需要重新分配元素时,您可以尝试使用std::vector的方法 - 每次填满时将容量加倍,这具有摊销成本为O(1)

测试:

function test_MAT_vector

    LIM = 5e4;
    %%# Normal
    tic;
    A = zeros(1,1);
    for i = 1:LIM
        A(end+1) = i;
    end
    toc

    %%# std::vector clone
    tic;
    B = zeros(1,1);
    for i = 1:LIM
        if(i > numel(B))
            B = [B;zeros(numel(B),1)];
        end
        B(i) = i;
    end
    toc

end

输出:

经过 3.489820 秒的时间。

经过 0.006710 秒的时间。

使用单元格

%%# cell
tic;
A = cell(1,1);
for i = 1:LIM
    A(end+1) = {i};
end
toc

经过时间为2.740792秒。


有点让我感到难过,但那似乎是一个有效的解决方案。谢谢。 - Xzhsh
或者我应该用一个mex文件包装一个std::vector... <_< 但是那样我就得做一些持久化的工作。啊,算了吧。 - Xzhsh

2
在之前的问题中,我发布了一个类似的解决方案,与@Jacob提出的方案相似。
后来,我比较了大多数可用选项的性能(这些选项由@woodchips很好地总结)。以下是我在我的机器上获得的结果:
NUM = 50000;

%%# ========== MINE: ~0.07sec ==========
tic
BLOCK_SIZE = 2000;                           %# initial & increment size
listSize = BLOCK_SIZE;                       %# current list capacity
list = zeros(listSize, 2);                   %# actual list
listPtr = 1;                                 %# pointer to last free position
for i=1:NUM
    %# push items on list
    list(listPtr,:) = [rand rand];           %# store new item
    listPtr = listPtr + 1;                   %# increment position pointer
    %# add new block of memory if needed
    if( listPtr+(BLOCK_SIZE/10) > listSize ) %# less than 10%*BLOCK_SIZE free
        listSize = listSize + BLOCK_SIZE;    %# add new BLOCK_SIZE slots
        list(listPtr+1:listSize,:) = 0;
    end
end
list(listPtr:end,:) = [];                    %# remove unused slots
toc

%%# ========== PREALLOCATION (matrix): ~0.05sec ==========
tic
list = zeros(NUM,2);
for i=1:NUM
    list(i,:) = [rand rand];
end
toc

%%# ========== PREALLOCATION (cell): ~1.1sec ==========
tic
list = cell(NUM,1);
for i=1:NUM
    list{i} = [rand rand];
end
list = vertcat( list{:} );
toc

%%# ============ NO-PREALLOCATION (grow cell): ~5sec ========
tic
list = {};
for i=1:NUM
    list{i} = [rand rand];
end
list = vertcat( list{:} );
toc

%%# ============ NO-PREALLOCATION (grow matrix): ~24sec ========
tic
list = [];
for i=1:NUM
    list(i,:) = [rand rand];
end
toc

%%# ========== GROWDATA (by John D'Errico): ~3.3sec =========
tic
growdata                             %# The initialization call
for i = 1:NUM
    growdata( [rand rand] )          %# push items
end
list = growdata;                     %# unpacking step
toc

1
我要补充说,简单的单元格方案对于相对较少的单元格数量是高效的。这是处理几千个单元格的好方法。即使在50k个单元格的情况下,这仍然很快。但当你开始进入数百万次的追加操作时,拥有数百万个单元格的单元格数组会变得更加昂贵。你仍然需要扩大指针数组。(我记得在早期版本的Matlab中情况不同,那时候我发现平衡点更低,所以我想他们已经随着时间的推移改进了事情。) - user85109
@woodchips:看来你说得对,如果将迭代次数增加几个零,那么不断增长的单元格解决方案的速度将会变慢得多。顺便说一句,我可能应该使用cell2mat(list)而不是vertcat(list {:}),因为它快上一个数量级.. - Amro

0

您可以使用可增长的单元数组,它比数组便宜得多,然后使用cell2mat将其转换为数组(需要两倍的内存,但如果您有它,这可能是最简单的方法)。

如果您知道像素数,可以预先分配正确大小的单元格数组,用实际值的数组(最多640个,但通常为2-5个)填充每个单元格,然后使用cell2mat将其转换为连续数组。

如果您担心碎片化,可以在加载单元格后执行pack,这应该对内存进行碎片整理(将所有内容写入磁盘并再次以连续方式加载),然后再执行cell2mat转换。


是的,但它很慢(我在我的答案中进行了剖析)。 - Jacob
@Jacob:对于像你的例子这样的单个元素,它很慢,但是当我想要连接更大的数组时,我通常使用那种方法,因为数组扩展真的会让你感到痛苦...例如,在你的测试中,不要添加单个元素,而是添加一个长度为100000-1000000的随机数组。此外,您可以预分配单元格数组。我认为您会发现这比数组扩展快得多,但我确信仍然不如您的std :: vector克隆快(尽管在这种情况下有点更复杂)。 - robince

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