考虑以下列方式获得的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
,这需要时间和内存。
您能否建议一种能够使parfor
比for
更方便的解决方案?我认为保存baseA
中的所有单元格是一种通过在开头加载一次来节省时间的方法,但也许我错了。
答案 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
进行每次单独迭代的时间会增加,但总时间会大幅下降。
这张照片显示了我在家用电脑上运行测试的结果。我使用了n=1000
和eig(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
循环,普通parfor
和parfor
切片数据执行此任务,并将比较时间。
请记住,这是一个玩具示例,我对替代解决方案不感兴趣,例如bsxfun()
,这不是分析的重点。
借用Adriaan中相同类型的情节,我首先确认关于普通parfor
与for
的相同结果。其次,这两种方法在切片数据上都被parfor
完全优于,在一千万行的数据集上完成需要2秒多一点(切片操作包含在时间中) )。普通parfor
需要24秒才能完成,for
几乎需要两倍的时间(我在Win7 64,R2016a和i5-3570有4个核心)。
在开始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}}。
然而,这是一个玩具示例,其中内循环的成本完全由索引操作占用。因此,对于值parfor
,内循环应该更复杂和/或展开。
现在,假设你的内部循环更复杂并且简单的parfor
循环更慢,让我们看一下我们通过避免4个worker和50万行数据集中的广播数据来节省多少内存(RAM中大约760 MB)。
如您所见,向工作人员发送了近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}