使用parfor节省时间和内存?

时间:2015-08-21 17:55:14

标签: matlab optimization parallel-processing parfor

考虑以下列方式获得的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的创建也更复杂]。

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

您能否建议一种能够使parforfor更方便的解决方案?我认为保存baseA中的所有单元格是一种通过在开头加载一次来节省时间的方法,但也许我错了。

2 个答案:

答案 0 :(得分:32)

一般信息

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

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

检查this question以获取有关在不同工作人员之间拆分数据的信息。

基准

代码

请考虑以下示例,以查看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进行每次单独迭代的时间会增加,但总时间会大幅下降。

结果

parforbenchtest

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

答案 1 :(得分:11)

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

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

此类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循环,普通parforparfor 切片数据执行此任务,并将比较时间。

请记住,这是一个玩具示例,我对替代解决方案不感兴趣,例如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')

您可以在github repo上找到cache2cell()

对切片数据的简单

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

enter image description here

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

使用切片的parfor

保存内存

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

enter image description here

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

切成一个单元格

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

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

data

out切片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}