使用 parfor 节省时间和内存?

Saving time and memory using parfor?

考虑prova.mat在MATLAB中通过以下方式得到

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')

为了了解我的数据的实际维度,A1 中的 1x9 cell 由以下 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 中 运行 parfor4 本地工作人员的循环需要 1.2 秒。用 for 替换 parfor 需要 0.01 秒。

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

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

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

一般信息

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

当不使用隐式多线程函数之一时 parfor is basically recommended in two cases: lots of iterations in your loop (i.e., like 1e10), or if each iteration takes a very long time (e.g., eig(magic(1e4))). In the second case you might want to consider using spmd (slower than parfor in my experience). The reason parfor is slower than a for 短循环 运行ges 或快速迭代是正确管理所有工作人员所需的开销,而不是仅仅进行计算。

查看 以了解有关在不同工作人员之间拆分数据的信息。

基准测试

代码

考虑以下示例以了解 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.27 秒到 0.39 秒)。但是请注意,parfor 使用了所有可用的工作线程,因此总时间 (sum(Time)) 必须除以计算机的核心数。所以对于我来说,总时间从大约 270 秒减少到 49 秒,因为我有一个八核处理器。

因此,虽然使用 parfor 进行每个单独迭代的时间比使用 for 增加了,但总时间却大大减少了。

结果

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

将广播数据切片到元胞数组中

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

此类 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 和带有 sliced 数据的 parfor 来执行此任务,并将比较时间。

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

结果

Adriaan 借用相同类型的图,我首先确认关于普通 parforfor 的相同发现。其次,这两种方法在切片数据上 完全优于 parfor,在具有 1000 万行的数据集上完成切片操作需要 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 实际上 快一点 parfor.

但是,这是一个玩具示例,其中内部循环的成本完全由索引操作承担。因此,为了让 parfor 有价值,内部循环应该 更复杂 and/or 展开。

使用切片 parfor 节省内存

现在,假设您的内部循环更复杂,而简单的 for 循环更慢,让我们看看通过避免在具有 4 个 worker 的 parfor 和具有 50 个 worker 的数据集中广播数据,我们节省了多少内存百万行(约 760 MB 内存)。

如您所见,将近 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