【问题标题】:How to boost up a for-loop如何提升 for 循环
【发布时间】:2019-06-13 01:07:50
【问题描述】:

我在 R 中遇到了缓慢的 for 循环执行。这里我提供了我的一部分代码,它会产生延迟。

## subsitutes for original data
DC <- matrix(rnorm(10), ncol=101, nrow=6400)
C <- matrix(rnorm(20), ncol=101, nrow=6400)


N <- 80
Vcut <- ncol(DC) 
V <- seq(-2.9,2.5,length=Vcut)
fNC <- matrix(NA, nrow=(N*N), ncol=Vcut)
fNDC <- matrix(NA, nrow=(N*N), ncol=Vcut)


Arbfunc <- function(dV){

b <- matrix(NA, nrow=1, ncol=Vcut)

  for(i in 1:(N*N)) {
    for (n in 1:Vcut) {
      for (k in 1:Vcut) {
        b[k] = (V[2]-V[1])*(exp((-1)*abs(V[k])))*exp(abs(V[n]-V[k])/dV)*(C[i,k]/V[k])
      }
      fNC[i,n] = exp(1*abs(V[n]))*(1/(2*dV))*(sum(b[]))
      fNDC[i,n] = DC[i,n]/fNC[i,n]
    }
  }   
}

Arbfunc(0.5)

由于我需要比较 dV's 的各种值之间的结果,因此该代码至少应在几秒钟内运行。但结果是

user   system  elapsed
40.15   0.03   40.24

对于足够的比较来说太慢了。我尝试了几种并行化方法,但结果并不令人满意(40 -> 25 秒,尽管我在我的电脑中使用了 11 个线程)。

因此,我的猜测是瓶颈在于这个 for 循环本身,而不是非并行代码。你能给我一些建议来改进这个 for 循环或提示并行化吗?简短的评论将不胜感激。

【问题讨论】:

  • 你考虑过使用apply/sapply/tapply吗?
  • 您想要达到的数学公式是什么?如果您可以在矩阵/行/列而不是单个元素上使用absexp 等函数重写它(即用 R 术语对其进行矢量化),您将获得显着的加速。由于您已经预先分配了矩阵,我怀疑使用 *apply 函数会有很大帮助。如果您可以使用矩阵乘法等重写它,它可能会使用底层的 fortran 库并行运行。像(V[1]-V[2]) 这样的简单常量显然可以被拉到循环之外。
  • 非常感谢 cmets,让我试试 apply 功能

标签: r for-loop parallel-processing


【解决方案1】:

非常感谢 @Mikko Marttila 更正了函数 3 和 4 并提供了函数 5 的想法。

最好使用矢量化选项而不是显式循环来处理 R。比如k的内循环:

for (k in 1:Vcut) {
  b[k] = (V[2]-V[1])*(exp((-1)*abs(V[k])))*exp(abs(V[n]-V[k])/dV)*(C[i,k]/V[k])
}

这和说的一样

(V[2]-V[1])*(exp((-1)*abs(V)))*exp(abs(V[n]-V)/dV)*(C[i,]/V)

这个小改动让我们这部分函数的性能提升了 500 倍:

Unit: microseconds
         expr     min      lq      mean  median      uq     max neval
       k_loop 13186.7 13603.2 14605.471 13832.9 14517.8 41935.1   100
 k_vectorized    16.4    17.6    25.559    28.8    32.0    52.7   100

现在,如果我们用 i 查看外部循环,我们会发现实际上没有必要逐行循环。我们可以为 sum(b[k]) 语句创建一个矩阵,将其变为:

(V[2]-V[1])*(exp((-1)*abs(V)))*exp(abs(V[n]-V)/dV)*(C[i,]/V)

进入这个:

(V[2]-V[1])*(exp((-1)*abs(V)))*exp(abs(V[n]-V)/dV)*(t(C)/V)

这只是为我们节省了N*N*k 循环。在您的情况下,这是 646,400 个循环。

总而言之,我们会:

Arbfunc3 <- function(dV){
    for (n in 1:Vcut) {
      sum_b = colSums((V[2]-V[1])*(exp((-1)*abs(V)))*exp(abs(V[n]-V)/dV)*(t(C)/V))
      fNC[, n] = exp(1*abs(V[n]))*(1/(2*dV))*(sum_b)
      fNDC[, n] = DC[,n]/fNC[,n]
    }
}

对于这个替代方案,我进行微基准测试的中位时间是 750 毫秒。

为了进一步提高性能,我们需要解决V[n] - V。值得庆幸的是,R 有一个函数 - outer(V, V, '-'),这将生成一个包含我们需要的所有组合的矩阵。

Arbfunc4 <- function(dV) {
  sum_b = apply((V[2]-V[1])*(exp((-1)*abs(V)))*exp(abs(outer(V, V, '-')) / dV) / V, 2, function(x) colSums(x * t(C)))

  fNC = exp(1*abs(V))*(1/(2*dV))*t(sum_b)
  fNDC= DC/t(fNC)
  fNDC
}

感谢@Mikko Marttila 提出的摆脱应用点积的建议。

Arbfunc5 <- function(dV) {
  a = (V[2] - V[1]) * exp(-abs(V)) * t(C) / V
  b = exp(abs(outer(V, V, "-")) / dV) %*% a

  fNC = exp(1*abs(V))*(1/(2*dV))*(b)
  fNDC= DC/t(fNC)
  fNDC
}

这是每个解决方案的 system.time(Arbfunc2 是消除 k_loop)。优化后的解决方案比原始解决方案快 2,600 倍。

> system.time(Arbfunc(0.5))
   user  system elapsed 
  78.03    0.39   79.72 
> system.time(Arbfunc2(0.5))
   user  system elapsed 
  10.41    0.03   10.46 
> system.time(Arbfunc3(0.5))
   user  system elapsed 
   0.69    0.13    0.81 
> system.time(Arbfunc4(0.5))
   user  system elapsed 
   0.43    0.05    0.47 
> system.time(Arbfunc5(0.5))
   user  system elapsed 
   0.03    0.00    0.03 

最终编辑:这是我在重新启动 R 并清空环境后运行的完整代码。没有错误:

## subsitutes for original data
DC <- matrix(rnorm(10), ncol=101, nrow=6400)
C <- matrix(rnorm(20), ncol=101, nrow=6400)

N <- 80
Vcut <- ncol(DC) 
V <- seq(-2.9,2.5,length=Vcut)

# Unneeded for Arbfunc4 adn Arbfunc5
# Corrected from NA to NA_real_ to prevent coercion from logical to numeric
# h/t to @HenrikB
fNC <- matrix(NA_real_, nrow=(N*N), ncol=Vcut)
fNDC <- matrix(NA_real_, nrow=(N*N), ncol=Vcut)

Arbfunc <- function(dV){
  b <- matrix(NA, nrow=1, ncol=Vcut)

  for(i in 1:(N*N)) {
    for (n in 1:Vcut) {
      for (k in 1:Vcut) {
        b[k] = (V[2]-V[1])*(exp((-1)*abs(V[k])))*exp(abs(V[n]-V[k])/dV)*(C[i,k]/V[k])
      }
      fNC[i,n] = exp(1*abs(V[n]))*(1/(2*dV))*(sum(b[]))
      fNDC[i,n] = DC[i,n]/fNC[i,n]
    }
  }
  fNDC
}

Arbfunc2 <- function(dV){
  b <- matrix(NA, nrow=1, ncol=Vcut)

  for(i in 1:(N*N)) {
    for (n in 1:Vcut) {
      sum_b = sum((V[2]-V[1])*(exp((-1)*abs(V)))*exp(abs(V[n]-V)/dV)*(C[i,]/V))
      fNC[i,n] = exp(1*abs(V[n]))*(1/(2*dV))*(sum_b)
      fNDC[i,n] = DC[i,n]/fNC[i,n]
    }
  }
  fNDC
}

Arbfunc3 <- function(dV){
  for (n in 1:Vcut) {
    sum_b = colSums((V[2]-V[1])*(exp((-1)*abs(V)))*exp(abs(V[n]-V)/dV)*(t(C)/V))
    fNC[, n] = exp(1*abs(V[n]))*(1/(2*dV))*(sum_b)
    fNDC[, n] = DC[,n]/fNC[,n]
  }
  fNDC
}

Arbfunc4 <- function(dV) {
  sum_b = apply((V[2]-V[1])*(exp((-1)*abs(V)))*exp(abs(outer(V, V, '-')) / dV) / V, 2, function(x) colSums(x * t(C)))

  fNC = exp(1*abs(V))*(1/(2*dV))*t(sum_b)
  DC/t(fNC)
}

Arbfunc5 <- function(dV) {
#h/t to Mikko Marttila for dot product
  a = (V[2] - V[1]) * exp(-abs(V)) * t(C) / V
  b = exp(abs(outer(V, V, "-")) / dV) %*% a

  fNC = exp(1*abs(V))*(1/(2*dV))*(b)
  DC/t(fNC)
}

#system.time(res <- Arbfunc(0.5))
system.time(res2 <- Arbfunc2(0.5))
system.time(res3 <- Arbfunc3(0.5))
system.time(res4 <- Arbfunc4(0.5))
system.time(res5 <- Arbfunc5(0.5))

all.equal(res2,res3,res4,res5)

正如@HenrikB 提到的,fNCfNDC 初始化为逻辑矩阵。这意味着我们在将它们强制为real 矩阵时会受到性能影响。对这个数据集执行不正确的操作是 1 毫秒的一次性命中,但如果这种强制处于循环中,它真的可以加起来。

mat_NA_real_ <- function() {
  mat = matrix(NA_real_, nrow = 6400, ncol = 101)
  mat[1,1] = 1
}

mat_NA <- function() {
  mat = matrix(NA, nrow = 6400, ncol = 101)
  mat[1,1] = 1
}
microbenchmark(mat_NA_real_(), mat_NA())

Unit: microseconds
           expr    min      lq     mean  median     uq     max neval
 mat_NA_real_()  979.5  992.25 1490.081  998.65 1021.1  7612.5   100
       mat_NA() 1865.8 1883.30 3793.119 1911.30 5335.4 53635.2   100

【讨论】:

  • 是的,这就是我要发布的内容,很好的答案!在向量中思考可以使代码更简单、更快。
  • 不错的答案。我认为您需要在Argfunc3() 中转置C 矩阵,然后采用colSums() 而不是rowSums()。这是因为原始循环将C 中的每一行与V 分开,但在没有转置的情况下,C / V 将回收V 并将C 的列分开。在这种情况下,结果恰好是相同的,因为 C 是通过重复 20 个值构造的,这使得总和相等,但这在一般情况下不成立。
  • 您也可以将apply() 写成矩阵乘积,使用a = (V[2] - V[1]) * exp(-abs(V)) * t(C) / Vb = exp(abs(outer(V, V, "-")) / dV) %*% a
  • 是的,可能是 fp 错误。对于这样的计算,您通常最好与all.equal() 进行比较而不是identical()
  • MEMORY => 速度:现在,当您使用 matrix(NA, ...) 时,您正在分配逻辑矩阵。确保分配数字矩阵,即matrix(NA_real_, ...)。有关说明,请参阅 jottr.org/2014/06/17/matrixna-wrong-way
猜你喜欢
  • 2023-04-07
  • 1970-01-01
  • 2013-07-12
  • 2020-07-21
  • 2022-11-22
  • 1970-01-01
  • 1970-01-01
  • 2018-03-05
  • 2021-10-28
相关资源
最近更新 更多