【问题标题】:Saving time and memory using parfor?使用 parfor 节省时间和内存?
【发布时间】:2015-11-15 18:23:58
【问题描述】:

考虑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 中使用 4 本地工作人员运行 parfor 循环,则需要 1.2 秒。将 parfor 替换为 for 需要 0.01 秒。

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

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

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

【问题讨论】:

  • 在任何情况下,parfor 都不能比常规 for 循环更快,如果并行分布值得付出代价,即您有足够的计算量来进行,它会有所改进
  • 嗯,每次迭代可能会花费更多时间(在我今天进行的基准测试中大约需要 20%),但由于您使用了更多内核,因此总时间会减少。跨度>
  • 与您的问题无关,但我相信在您的第一个块中,您可以(并且应该!)再次使用动态字段名称来避免 eval:baseA.(['A' num2str(w)])= baseA_;

标签: matlab optimization parallel-processing parfor


【解决方案1】:

一般信息

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

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

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

基准测试

代码

考虑以下示例以查看 forparfor 的行为相对的行为。如果您还没有这样做,请先打开并行池:

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 使用了所有可用的worker,因此总时间(sum(Time))必须除以计算机中的内核数。所以就我而言,总时间从 270 秒左右下降到 49 秒,因为我有一个八核处理器。

因此,虽然使用parfor 进行每次单独迭代的时间相对于使用for 有所增加,但总时间却大大减少了。

结果

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

【讨论】:

  • 您的答案似乎自相矛盾...我很确定eig 大部分时间都在那里,它应该在 MATLAB R2012a 中是多线程的...你的总时间怎么来的那么除以 3.25 呢?
  • @reverse_engineer 抱歉,我花了这么长时间才回复你;我刚刚在 MATLAB 自己的文档中找到了thiseig 也被他们用作示例。 eig 没有出现在我在答案中链接的多线程函数列表中,所以我无法帮助你;你应该问问 TMW 自己是否是。
【解决方案2】:

将广播数据切片成一个元胞数组

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

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,在具有 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

【讨论】:

  • 有趣的切片方式。在 Jonas 的建议下,我将自己的数据(约 8 亿行)切成小块并将其存储在磁盘上,然后将每一块加载到所需的 worker 上。这也减少了内存使用,但可能会比你的方法慢。
  • @Adriaan 总的来说,我确实处理了太多的数据,以至于它不适合内存(超过 TB),当我需要部分预先计算的结果时,我会使用这种切片/缓存可用于我从磁盘加载的每个文件。我基本上都在使用这两种方法。
猜你喜欢
  • 2018-01-27
  • 1970-01-01
  • 1970-01-01
  • 2016-04-04
  • 2021-12-04
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多