嵌套foreach公司循环

史蒂夫·韦斯顿

由Hong Ooi转换为RMarkdown

介绍

这个foreach公司包提供了一个循环构造,用于重复执行R代码。与标准类似对于循环,这使得转换对于循环到foreach公司循环。与R的许多并行编程包不同,foreach公司不需要主体对于循环转换为函数。foreach公司不同于对于循环,它的返回是一个值列表,而对于循环没有值,并使用副作用来传递其结果。因此,foreach公司循环比对于当循环的目的是创建数据结构(如向量、列表或矩阵)时,则执行循环:首先,代码重复较少,因此出错的可能性较小,因为不需要初始化向量或矩阵。第二,aforeach公司通过只更改单个关键字,可以轻松地并行化循环。

嵌套操作符:%:%

的一个重要特征foreach公司%:%操作员。我把这个叫做嵌套运算符,因为它用于创建嵌套foreach公司循环。就像%完成%%多巴%运算符,它是一个二进制运算符,但它对两个运算符进行操作foreach公司物体。它还返回一个foreach公司对象,本质上是其操作数的特殊合并。

假设我们想使用一个名为模拟(记住模拟需要相当密集的计算才能值得并行执行。)这个模拟函数有两个参数,我们想用存储在向量中的所有值的组合来调用它avec公司bvec公司。以下双重嵌套对于循环可以做到这一点。出于测试目的模拟函数被定义为返回\(10a+b\)(当然,如此琐碎的操作不值得并行执行。)

x个<- 矩阵(0,长度(avec),长度(bvec))
对于(j)在里面 1:长度(bvec)){
  对于(i)在里面 1:长度(avec)){
x[i,j]<- 模拟(avec[i],bvec[j])
}
}
x个
##      [,1] [,2] [,3] [,4]## [1,]   11   12   13   14## [2,]   21   22   23   24

在这种情况下,将结果存储在矩阵中是有意义的,因此我们创建了一个合适大小的矩阵,称为x个,并分配的返回值模拟到适当的元素x个每次都通过内部循环。

使用时foreach公司,我们不创建矩阵并为其赋值。相反,内部循环将结果矩阵的列作为向量返回,这些向量在外部循环中组合为矩阵。下面是如何使用%:%操作员。由于运算符的优先级,不能在内部foreach公司循环。

x个<-
  foreach公司(b条=bvec、,.联合收割机='cbind')%:%
    foreach公司(一个=avec、,.联合收割机=“c”)%完成%{
      模拟(a、b)
}
x个
##结果。1个结果。2个结果。3个结果。4## [1,]       11       12       13       14## [2,]       21       22       23       24

这与嵌套的对于循环。外部foreach公司正在迭代中的值bvec公司将它们传递到内部foreach公司,它迭代中的值avec公司对于的每个值bvec公司因此模拟在这两种情况下,函数的调用方式相同。此版本中的代码稍微干净一些,并且具有易于并行化的优点。

使用%:%具有%多巴胺%

并行化嵌套时对于循环,总是存在一个问题,即要并行化哪个循环。标准建议是并行化外部循环。这会产生较大的单个任务,较大的任务通常比较小的任务执行效率更高。然而,如果外循环没有很多迭代,并且任务已经很大,那么并行化外循环会导致少量的大型任务,这可能不允许您使用所有处理器,也可能导致负载平衡问题。您可以改为并行化内部循环,但这可能效率低下,因为您每次都在重复等待通过外部循环返回所有结果。如果任务和迭代次数的大小不同,那么很难知道要并行化哪个循环。

但在我们的蒙特卡洛示例中,所有任务都是完全独立的,因此它们都可以并行执行。您确实希望将循环视为指定单个任务流。您只需要小心地正确处理所有结果,这取决于它们来自内部循环的哪个迭代。

这正是%:%操作员做了:它会转多圈foreach公司循环成单个循环。这就是为什么只有一个%完成%运算符。当我们并行化嵌套foreach公司通过更改%完成%%多巴胺%,我们正在创建一个可以并行执行的任务流:

x个<-
  foreach公司(b条=bvec、,.联合收割机=“cbind”)%:%
    foreach公司(一个=avec、,.联合收割机=“c”)%多巴胺%{
      模拟(a、b)
}
x个
##结果。1个结果。2个结果。3个结果。4## [1,]       11       12       13       14## [2,]       21       22       23       24

当然,我们实际上只会并行运行与处理器数量相同的任务,但并行后端会处理所有这一切。关键是%:%运算符可以轻松指定要执行的任务流.联合收割机的参数foreach公司允许我们指定如何处理结果。后端处理并行执行的任务。

分块任务

当然,在这方面肯定会有障碍。如果任务非常小,那么您可能真的希望将整个内部循环作为单个任务执行,该怎么办?好吧,即使对于单节点循环来说,小任务也是一个问题。这个问题的解决方案,无论您有一个单独的循环还是嵌套的循环,都是使用任务分块.

任务分块允许您一次向工作人员发送多个任务。这可能会更有效率,尤其是对于短期任务。目前,只有doNWS公司后端支持任务分块。以下是如何完成的doNWS公司:

opts选项<- 列表(块大小=2)
x个<-
  foreach公司(b=bvec、,.联合收割机=“cbind”,.options.nws(选项)=选项)%:%
    foreach公司(一个=avec、,.联合收割机=“c”)%多巴胺%{
      模拟(a、b)
}
x个
##结果。1个结果。2个结果。3个结果。4## [1,]       11       12       13       14## [2,]       21       22       23       24

如果你不使用doNWS公司,则忽略此参数,这允许您编写独立于后端的代码。您还可以为多个后端指定选项,并且只使用与已注册后端匹配的选项列表。

如果能自动选择块大小,那就太好了,但我还没有想出一个好的、安全的方法。因此,现在需要手动指定块大小。

关键是通过使用%:%运算符,可以转换嵌套对于循环到嵌套foreach公司循环,使用%多巴胺%并行运行,然后使用块大小选项,以便它们足够大,能够有效执行,但不会太大,导致负载平衡问题。您不必担心要并行化哪个循环,因为您正在将嵌套的循环转换为单个任务流,这些任务流都可以由并行后端并行执行。

另一个例子

现在让我们想象一下模拟函数返回包含错误估计的对象。我们希望返回每个b值的错误最小的结果,以及生成该结果的参数。下面是如何使用嵌套对于循环:

n个<- 长度(bvec)
d日<- 数据帧(x个=数字(n) ,一个=数字(n) ,b条=数字(n) ,错误=数字(n) )

对于(j)在里面 1:n){
错误<- Inf公司
最好的<- 无效的
  对于(i)在里面 1:长度(avec)){
对象<- 模拟(avec[i],bvec[j])
    如果(对象$错误<错误){
错误<-对象$错误
最好的<- 数据帧(x个=对象$x、,一个=avec[i],b条=bvec[j],错误=对象$错误)
}
}
日[j,]<-最好的
}
d日
##x a b错误## 1 11 1 1   0## 2 22 2 2   0## 3 23 2 3   1## 4 24 2 4   2

这也很容易转换为foreach公司。我们只需要提供适当的.联合收割机功能。对于外部foreach公司,我们可以使用标准旋转可用于数据帧的函数。对于内部foreach公司,我们编写了一个函数来比较两个数据帧,每个数据帧有一行,并返回错误估计值较小的数据帧:

梳子<- 功能(d1,d2)如果(d1)$错误<第2天$错误)d1其他的第2天

现在我们用.联合收割机内心的争论foreach公司:

opts选项<- 列表(块大小=2)
d日<-
  foreach公司(b条=bvec、,.联合收割机='绑定',.options.nws(选项)=选项)%:%
    foreach公司(一个=avec、,.联合收割机=“梳”,.顺序=错误的)%多巴胺%{
对象<- 模拟(a、b)
      数据帧(x个=对象$x、,一个=a、,b条=b、,错误=对象$错误)
}
d日
##x a b错误## 1 11 1 1   0## 2 22 2 2   0## 3 23 2 3   1## 4 24 2 4   2

注意,由于梳子函数不重要,我已将.顺序的参数错误的。这减少了需要保存在主控形状上的结果的数量,以便在它们被无序返回时进行组合。但即使有并行化、特定于后端的选项和.顺序参数,嵌套foreach公司这个版本很可读。

但如果我们想将指数返回到avec公司bvec公司而不是数据本身?一种简单的方法是创建两个计数迭代器,然后传递给foreach公司功能:

图书馆(迭代器)
opts选项<- 列表(块大小=2)
d日<-
  foreach公司(b条=bvec、,j个=icount(图标)(),.联合收割机='绑定',.options.nws(选项)=选项)%:%
    foreach公司(一个=avec、,我=icount(图标)(),.联合收割机=“梳”,.顺序=错误的)%多巴胺%{
对象<- 模拟(a、b)
      数据帧(x个=对象$x、,我=我,j个=j、,错误=对象$错误)
}
d日
##x i j错误## 1 11 1 1   0## 2 22 2 2   0## 3 23 2 3   1## 4 24 2 4   2

注意,对icount的调用作为参数传递给foreach公司。如果迭代器被创建并传递给foreach公司例如,使用一个变量,我们将无法获得所需的效果。这不是错误或限制,而是foreach公司功能。

这些新迭代器是无限迭代器,但这没有问题,因为我们已经bvec公司avec公司以控制循环的迭代次数。使其无限意味着我们不必使其与同步bvec公司avec公司.

结论

嵌套对于循环是一种常见的构造,通常是R脚本中最耗时的部分,因此它们是并行化的主要候选。通常的方法是并行化外部循环,但正如我们所看到的,由于任务的大小和数量之间的不平衡,这可能会导致性能不佳。通过使用%:%操作员foreach公司,并且通过使用分块技术,可以克服许多这些问题。生成的代码通常比原始R代码更清晰易读,因为foreach公司正是为了解决这类问题而设计的。