R 迴圈 List, Vector 記憶體預分配的考量

在 PTT R_Language 版有一篇 2015 年的文章「[心得] 預分配記憶體的差異」,比較了 R 迴圈時 List 變數是否預分配記憶體的計算速度差異。作者使用的 R 版本是 Revolution R Open 3.2.0,結論是速度相差 60 倍。但最近幾年 R 改版幅度變化很大,國外有一些新的說法出現,認為 List 變數的記憶體本來就不是連續配置,所以在每個迴圈中逐次增加 List 變數元素,在計算速度的影響不大。

以下是我把該文測試 R 程式用幾個較新 R 版本測試的結果

先看結果。底下表格是 List 變數在迴圈之前是否作「記憶體預分配」的計算時間比較。PRO 3.2.0 是指 Revolution R 公司尚未賣給微軟之前的 Community 版本,已經有一些向量矩陣平行運算的功能:

 

從上表中可以看出,相較於 PRO 3.2.0 與 R 3.3.2,在 R 3.4.0 版(含)之後,由於內建 JIT 自動編譯,是否作記憶體預分配的差異已經不是很大。

接下來是向量 (Vector) 在迴圈之前是否作記憶體預分配的計算時間比較:

 

上表可以看出:在迴圈之前有作記憶體預分配的向量計算程式的確快很多,而且在 R 3.4.0 版(含) 之後,速度的提升非常顯著,從 PRO 3.2.0 的 53 倍差距,提升到 452 倍、460 倍的差距。

附註:

測試程式沿用 2015 年 PTT 文章的程式,僅做 functions 名稱的修改

List 變數:

L_NO <- function(){
    a <- list()
    for (i in 1:1e6){ a[[i]] <- rnorm(10) }
}

L_pre <- function(){
    a <- vector('list', 1e6)
    for (i in 1:1e6){ a[[i]] <- rnorm(10) }
}
library(rbenchmark)
benchmark(L_NO(), L_pre(),
columns = c("test", "replications", "elapsed", "relative"),
order = "relative", replications = 20)

Vector 變數:

V_NO <- function(){
  MaxIter = 1e5
  x = c()
  i = 0
  while (i < MaxIter){
    i <- i + 1
    x <- c(x, i)
    if (i > 5e4)
      break
  }
  x
}

V_pre <- function(){
  MaxIter = 1e5
  x = rep(NA_real_, MaxIter) 
  for (i in 1:MaxIter) {
    x[i] <- i
    if (i > 5e4)
      break
  }
  (x = x[!is.na(x)])
}

library(rbenchmark)
benchmark(V_NO(), V_pre(),
columns = c("test", "replications", "elapsed", "relative"),
order = "relative", replications = 20)