StatsBeginner: 初学者の統計学習ノート

統計学およびR、Pythonでのプログラミングの勉強の過程をメモっていくノート。たまにMacの話題。

Rで{snow}と{parallel}の並列化を少し試してみた

Rで計算を高速化したいとき、

  • なるべくベクトル計算にしてforで頭からみていくような処理を避ける
  • 自作関数をコンパイルする
  • 並列化(マルチコアの利用)

などの手法があり、ベクトル化とコンパイルに関してはケースによって何が有効かというのは難しい。
ただ、forで頭から順に見ていく処理(前後の依存関係がなく、処理順を入れ替えたり、カタマリに分割しても問題がないようなもの)をやる場合に、複数のスレッドに分けて並行処理すればその分速くなるというのは直観的に理解しやすい。


色々試したわけではないので、どういう場合にどの程度並列化が有効であるかとかはよく分かってないのだが、取り急ぎコードの書き方を忘れないため程度のメモとして、以下に書き写しておく。
なお、並列化すると、時間を測った場合のuserとsystemの値がおかしくなるが、elapsedの値は合っている。(一応ストップウォッチで確認した。)

> library(snow)
> library(parallel)
> detectCores(logical = FALSE)  # 物理コア数の確認
[1] 4
> detectCores(logical = TRUE)  # 論理コア数の確認
[1] 8
> 
> # 処理の中身はどうでもいいが、ここでやっている処理が何かというと、
> # faid_allという文字列のベクトルがあり、これを頭から順にみていって、
> # d_all3というデータフレームのfile_article_idという列の値と一致する
> # 行の、content_without_tagという列に入っているテキストを連結して
> # text_allという新しいベクトルに追加していく。

> # 並列化しない場合
> t <- proc.time()
> text_all <- c()
> for(i in 1:length(faid_all)) {
+   target.rows <- which(d_all3$file_article_id==faid_all[i])
+   text.sep <- d_all3[target.rows,]$content_without_tag
+   text.paste <- paste(text.sep, collapse = '\n')
+   text_all <- c(text_all2, text.paste)
+ }
> proc.time()-t
   user  system elapsed 
 64.819  12.420  77.050 

> # {snow}のsocketで並列化
> cl <- makeCluster(8, type="SOCK")  # クラスターを8つ立ち上げ
> clusterExport(cl, c('d_all3','faid_all')) # オブジェクトをクラスターにコピー
> # クラスターを立ち上げてオブジェクトをコピーするところで10秒ぐらいかかるので、時間はここから測る。
> t <- proc.time()
> text_all <- parSapply(cl, faid_all, function(p){
+   target.rows <- which(d_all3$file_article_id==p)
+   text.sep <- d_all3[target.rows,]$content_without_tag
+   text.paste <- paste(text.sep, collapse = '\n')
+   return(text.paste)
+ })
> stopCluster(cl)  # クラスターを立ち上げ(忘れないように!)
> proc.time()-t
   user  system elapsed 
  0.098   0.054  16.365 

> # {parallel}のforkingで並列化
> t <- proc.time()
> text4 <- mclapply(faid_all, function(p){
+   target.rows <- which(d_all3$file_article_id==p)
+   text.sep <- d_all3[target.rows,]$content_without_tag
+   text.paste <- paste(text.sep, collapse = '\n')
+   return(text.paste)
+ }, mc.cores = 8)
> proc.time()-t
   user  system elapsed 
 36.652   8.208  16.077 


forkingとsocketの違いはあまり理解できてないが、socketは使用する変数を全て、各コアにコピーしなければならないらしく、そこの処理で少し余計に時間がかかる。
自分の場合、Macの物理コアが4、論理コアが8で、4本に並列化すると4倍ぐらいの速度にはなった。上の例のように8本にしても8倍にはならないのだが、4本よりは少し速かった。


以下は、AWSでvCPUが32個のマシンを借りて、とある処理にかかる時間を並列化の本数ごとに計測したもの。1回ずつしか測ってないので誤差がある。
途中からほとんど水平だが、並列化しても意味がないというより、たぶん「とある処理」の中で並列化と関係ない部分で30秒ぐらいかかってるんだろう。(vCPUは仮想CPUのことなので物理的なコア数のように比例的に増えないというのもあるかもしれないが。)