将广播数据切片成一个元胞数组
以下方法适用于按组循环的数据。分组变量是什么无关紧要,只要在循环之前确定即可。速度优势是巨大的。
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,在具有 1000 万行的数据集上完成需要 2 秒多一点的时间(切片操作包含在计时中)。普通的 parfor 需要 24 秒才能完成,for 几乎是这个时间的两倍(我使用的是 Win7 64、R2016a 和具有 4 核的 i5-3570)。
在启动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 实际上会比parfor略快。
但是,这是一个玩具示例,其中内部循环的成本完全由索引操作承担。因此,为了使parfor 有价值,内部循环应该更复杂 和/或展开。
使用切片 parfor 节省内存
现在,假设您的内部循环更复杂,而简单的for 循环更慢,让我们看看通过避免在具有 4 个工作人员的 parfor 和具有 5000 万行的数据集(对于大约 760 MB 的 RAM)。
如您所见,将近 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