R 中的向量化 for 循环

Nig*_*use 4 r

我正在努力矢量化以下函数的重复应用程序,我目前已将其实现为 for 循环。这个小例子表明了较大数据集的问题,矢量化将允许有益的运行时改进:

action = function(x,y,i) {

    firsttask = cumsum(x[which(x<y[i])])
    secondtask = mean(firsttask)
    thirdtask = min(firsttask[which(firsttask>secondtask)])
    fourthtask = length(firsttask)

    output = list(firsttask, data.frame(average=secondtask,
                                        min_over_mean=thirdtask,
                                        size=fourthtask))
    return(output)
}

thingofinterest = c(1:10)
limits = c(5:10)

test = vector("list", length = length(limits))
for(i in 1:length(limits)) {
test[[i]] = action(thingofinterest, limits, i)
}

test
Run Code Online (Sandbox Code Playgroud)

我想用矢量化命令替换 for 循环,而不是任何apply函数系列,因为它们并不总是能提高性能(我并不是说 for 循环有任何问题,我只需要优化此中的速度)案例。参见:R 的 apply 系列比语法糖更重要吗?)。我该怎么做?

Jos*_*ich 5

在开始尝试更改代码以使其更快之前,您需要了解代码中的瓶颈在哪里。例如:

timer <- function(action, thingofinterest, limits) {
  st <- system.time({           # for the wall time
    Rprof(interval=0.01)        # Start R's profile timing
    for(j in 1:1000) {          # 1000 function calls
      test = vector("list")
      for(i in 1:length(limits)) {
        test[[i]] = action(thingofinterest, limits, i)
      }
    }
    Rprof(NULL)  # stop the profiler
  })
  # return profiling results
  list(st, head(summaryRprof()$by.total))
}
action = function(x,y,i) {
  firsttask = cumsum(x[which(x<y[i])])
  secondtask = min(firsttask[which(firsttask>mean(firsttask))])
  thirdtask = mean(firsttask)
  fourthtask = length(firsttask)
  output = list(firsttask, data.frame(average=secondtask,
                                      min_over_mean=thirdtask,
                                      size=fourthtask))
  return(output)
}
timer(action, 1:1000, 50:100)
# [[1]]
#    user  system elapsed 
#   9.720   0.012   9.737 
# 
# [[2]]
#                 total.time total.pct self.time self.pct
# "system.time"         9.72    100.00      0.07     0.72
# "timer"               9.72    100.00      0.00     0.00
# "action"              9.65     99.28      0.24     2.47
# "data.frame"          8.53     87.76      0.84     8.64
# "as.data.frame"       5.50     56.58      0.44     4.53
# "force"               4.40     45.27      0.11     1.13
Run Code Online (Sandbox Code Playgroud)

您可以看到,在函数调用之外花费的时间非常少action。现在,for是一个特殊的基元,因此不会被探查器捕获,但探查器报告的总时间与实际时间非常相似,因此探查器时间不会丢失很多时间。

在函数中花费最多时间action的是对 的调用data.frame。去掉它,你就会得到巨大的加速。

action1 = function(x,y,i) {
  firsttask = cumsum(x[which(x<y[i])])
  secondtask = mean(firsttask)
  thirdtask = min(firsttask[which(firsttask>mean(firsttask))])
  fourthtask = length(firsttask)
  list(task=firsttask, average=secondtask,
    min_over_mean=thirdtask, size=fourthtask)
}
timer(action1, 1:1000, 50:100)
# [[1]]
#    user  system elapsed 
#   1.020   0.000   1.021 
# 
# [[2]]
#               total.time total.pct self.time self.pct
# "system.time"       1.01    100.00      0.06     5.94
# "timer"             1.01    100.00      0.00     0.00
# "action"            0.95     94.06      0.17    16.83
# "which"             0.57     56.44      0.23    22.77
# "mean"              0.25     24.75      0.13    12.87
# "<"                 0.20     19.80      0.20    19.80
Run Code Online (Sandbox Code Playgroud)

现在您还可以删除对 的一个调用mean和对 的两个调用which

action2 = function(x,y,i) {
  firsttask = cumsum(x[x < y[i]])
  secondtask = mean(firsttask)
  thirdtask = min(firsttask[firsttask > secondtask])
  fourthtask = length(firsttask)
  list(task=firsttask, average=secondtask,
    min_over_mean=thirdtask, size=fourthtask)
}
timer(action2, 1:1000, 50:100)
# [[1]]
#    user  system elapsed 
#   0.808   0.000   0.808 
# 
# [[2]]
#               total.time total.pct self.time self.pct
# "system.time"       0.80    100.00      0.12    15.00
# "timer"             0.80    100.00      0.00     0.00
# "action"            0.68     85.00      0.24    30.00
# "<"                 0.20     25.00      0.20    25.00
# "mean"              0.13     16.25      0.08    10.00
# ">"                 0.05      6.25      0.05     6.25
Run Code Online (Sandbox Code Playgroud)

现在您可以看到有“大量”时间花费在您的action职能之外的事情上。我用引号引起来,因为它占运行时间的 15%,但只有 120 毫秒。如果您的实际代码需要约 12 小时才能运行,则此新action函数将在约 1 小时内完成。

如果我在函数的循环test之外预先分配列表,结果会稍微好一点,但调用是最大的时间消耗。fortimerdata.frame