使用parfor节省时间和内存?

问题描述 投票:17回答:2

考虑以下列方式获得的MATLAB中的prova.mat

for w=1:100
    for p=1:9    
        A{p}=randn(100,1); 
    end
    baseA_.A=A;

    eval(['baseA.A' num2str(w) '= baseA_;'])

end

save(sprintf('prova.mat'),'-v7.3', 'baseA')

为了了解我的数据中的实际尺寸,1x9 cell中的A1由以下9数组组成:904x5, 913x5, 1722x5, 4136x5, 9180x5, 3174x5, 5970x5, 4455x5, 340068x5。其他Aj也有类似的组成。

请考虑以下代码

clear all
load prova
tic
parfor w=1:100
       indA=sprintf('A%d', w);
       Aarr=baseA.(indA).A;
       Boot=[];
       for p=1:9
           C=randn(100,1).*Aarr{p};
           Boot=[Boot; C];  
       end
       D{w}=Boot;
end
toc

如果我在我的Macbook Pro中使用parfor本地工作人员运行4循环,则需要1.2秒。用parfor替换for需要0.01秒。

根据我的实际数据,时间差为31秒对7秒[矩阵C的创建也更复杂]。

如果已正确理解问题是计算机必须向每个本地工作人员发送baseAto,这需要时间和内存。

你能否提出一个能让parforfor更方便的解决方案?我认为保存baseA中的所有细胞是一种通过在开始时加载一次来节省时间的方法,但也许我错了。

matlab optimization parallel-processing parfor
2个回答
35
投票

一般信息

很多函数都有implicit multi-threading built-in,使得parfor循环在使用这些函数时不比串行for循环更高效,因为所有内核都已被使用。在这种情况下,parfor实际上是有害的,因为它具有分配开销,同时与您尝试使用的功能并行。

当不使用其中一个隐式多线程函数时,基本上建议在两种情况下使用parfor:循环中的大量迭代(即,像1e10),或者如果每次迭代需要很长时间(例如,eig(magic(1e4)))。在第二种情况下,您可能需要考虑使用spmd(根据我的经验,慢于parfor)。 parfor比短距离或快速迭代的for循环慢的原因是正确管理所有工人所需的开销,而不是仅仅进行计算。

检查this question,了解有关在不同工作人员之间拆分数据的信息。

标杆

Code

请考虑以下示例,以查看for的行为,而不是parfor的行为。首先打开并行池,如果你还没有这样做:

gcp; % Opens a parallel pool using your current settings

然后执行几个大循环:

n = 1000; % Iteration number
EigenValues = cell(n,1); % Prepare to store the data
Time = zeros(n,1);
for ii = 1:n
tic
    EigenValues{ii,1} = eig(magic(1e3)); % Might want to lower the magic if it takes too long
Time(ii,1) = toc; % Collect time after each iteration
end

figure; % Create a plot of results
plot(1:n,t)
title 'Time per iteration'
ylabel 'Time [s]'
xlabel 'Iteration number[-]';

然后用parfor而不是for做同样的事情。您会注意到每次迭代的平均时间会增加(对于我的情况,为0.27s至0.39s)。但是要意识到parfor使用了所有可用的工作者,因此总时间(sum(Time))必须除以计算机中的核心数。所以对于我的情况,总时间从大约270s下降到49s,因为我有一个octacore处理器。

因此,虽然使用parfor进行每次单独迭代的时间相对于使用for,但总时间大幅下降。

Results

parforbenchtest

这张照片显示了我在家用电脑上运行测试的结果。我用过n=1000eig(500);我的电脑有一个I5-750 2.66GHz处理器,带有四个内核,运行MATLAB R2012a。正如你所看到的那样,并行测试的平均值大约在0.29s左右徘徊,而且序列代码相当稳定在0.24s左右。然而,总时间从234秒下降到72秒,这是3.25倍的加速。这不是4的原因是内存开销,如每次迭代所花费的额外时间所表示的那样。内存开销是由于MATLAB必须检查每个内核正在做什么,并确保每次循环迭代只执行一次,并且数据被放入正确的存储位置。


12
投票

将广播数据切片成单元阵列

以下方法适用于按组循环的数据。分组变量是什么并不重要,只要它在循环之前确定即可。速度优势是巨大的。

这种data的简化示例如下,第一列包含分组变量:

ngroups = 1000;
nrows   = 1e6;
data    = [randi(ngroups,[nrows,1]), randn(nrows,1)];
data(1:5,:)
ans =
          620     -0.10696
          586      -1.1771
          625       2.2021
          858      0.86064
           78       1.7456

现在,为简单起见,假设我对sum()感兴趣的是第二列中的值组。我可以按组循环,索引感兴趣的元素并总结它们。我将使用for循环,普通的parfor和带切片数据的parfor执行此任务,并将比较时间。

请记住,这是一个玩具示例,我对像bsxfun()这样的替代解决方案不感兴趣,这不是分析的重点。

结果

Adriaan借用相同类型的情节,我首先证实了关于普通parforfor的相同发现。其次,这两种方法都完全胜过切片数据上的parfor,在一千万行的数据集上完成需要2秒多一点(切片操作包含在时间中)。普通的parfor需要24秒才能完成,for几乎是这个时间的两倍(我在Win7 64,R2016a和i5-3570有4个核心)。

enter image description here

在启动parfor之前切片数据的要点是避免:

  • 整个数据的开销正在向工人广播,
  • 将操作索引到不断增长的数据集中。

代码

ngroups = 1000;
nrows   = 1e7;
data    = [randi(ngroups,[nrows,1]), randn(nrows,1)];

% Simple for
[out,t] = deal(NaN(ngroups,1));
overall = tic;
for ii = 1:ngroups
    tic
    idx     = data(:,1) == ii;
    out(ii) = sum(data(idx,2));
    t(ii)   = toc;
end
s.OverallFor = toc(overall);
s.TimeFor    = t;
s.OutFor     = out;

% Parfor
try parpool(4); catch, end
[out,t] = deal(NaN(ngroups,1));
overall = tic;
parfor ii = 1:ngroups
    tic
    idx     = data(:,1) == ii;
    out(ii) = sum(data(idx,2));
    t(ii)   = toc;
end
s.OverallParfor = toc(overall);
s.TimeParfor    = t;
s.OutParfor     = out;

% Sliced parfor
[out,t] = deal(NaN(ngroups,1));
overall = tic;
c       = cache2cell(data,data(:,1));
s.TimeDataSlicing = toc(overall);
parfor ii = 1:ngroups
    tic
    out(ii) = sum(c{ii}(:,2));
    t(ii)   = toc;
end
s.OverallParforSliced = toc(overall);
s.TimeParforSliced    = t;
s.OutParforSliced     = out;

x = 1:ngroups;
h = plot(x, s.TimeFor,'xb',x,s.TimeParfor,'+r',x,s.TimeParforSliced,'.g');
set(h,'MarkerSize',1)
title 'Time per iteration'
ylabel 'Time [s]'
xlabel 'Iteration number[-]';
legend({sprintf('for          : %5.2fs',s.OverallFor),...
        sprintf('parfor       : %5.2fs',s.OverallParfor),...
        sprintf('parfor_sliced: %5.2fs',s.OverallParforSliced)},...
        'interpreter', 'none','fontname','courier')

你可以在我的cache2cell()上找到github repo

Simple for on sliced data

您可能想知道如果我们在切片数据上运行简单的for会发生什么?对于这个简单的玩具示例,如果我们通过切片数据来取消索引操作,我们将删除代码的唯一瓶颈,而for实际上将比parfor更快。

enter image description here

然而,这是一个玩具示例,其中内循环的成本完全由索引操作获得。因此,为了使parfor值得,内环应该更复杂和/或展开。

用切片的parfor保存内存

现在,假设您的内部循环更复杂并且简单的for循环更慢,让我们看看我们通过避免4个工作人员和50万行数据集中的广播数据来节省多少内存(RAM中约760 MB) )。

enter image description here

如您所见,向工作人员发送了近3 GB的额外内存。切片操作需要一些内存来完成,但仍然比广播操作少得多,并且原则上可以覆盖初始数据集,因此一旦完成就承担可忽略的RAM成本。最后,切片数据上的parfor将仅使用一小部分内存,即与使用的切片相对应的量。

切成细胞

原始数据按组切片,每个部分存储在一个单元格中。由于单元格数组是一个引用数组,我们基本上将内存中连续的data分区为独立的块。

虽然我们的样本data看起来像这样

data(1:5,:)
ans =
          620     -0.10696
          586      -1.1771
          625       2.2021
          858      0.86064
           78       1.7456

切出的c看起来像

c(1:5)
ans = 
    [ 969x2 double]
    [ 970x2 double]
    [ 949x2 double]
    [ 986x2 double]
    [1013x2 double]

c{1}在哪里

c{1}(1:5,:)
ans =
            1      0.58205
            1      0.80183
            1     -0.73783
            1      0.79723
            1       1.0414
© www.soinside.com 2019 - 2024. All rights reserved.