並列計算パッケージsnowを使ったRでの並列計算

統計解析や数値解析を行う大学の研究室ではおなじみのR言語には、指定した処理を並列で行うための優れたパッケージ"snow"がしられています。

ここでは、snowパッケージを使ったRでの並列処理法について簡単に紹介します。  

パッケージの入手と読み込み

 > source("http://bioconductor.org/biocLite.R")
 > biocLite("snow")
 > library(snow)

並列環境での計算

ここでは、数値積分法を用いて近似的に円周率を求める手法について、4coreを使った並列計算を紹介します。

f:id:usskaji:20131110022229p:plain

初めに、積分範囲をN=5x106に分割して計算を行う関数を作成します。

 > N <- 5*10^6
 > func_calc <- function(i){
  x <- (i - 0.5) / N
  y <- 4.0 / (1.0 + x * x)
  return(y/N)}

つづいて並列化を行うコア数と、各コアに送る変数名(ここでは"N")を指定しましょう。

 > cl <- makeCluster(4, type="SOCK")
 > clusterExport(cl, "N")

以上で準備は完了です。それでは実際に計算させます。

 pi <- sum(parSapply(cl, 1:N, func_calc))

計算が終了したら、各コアに展開した環境を終了させます。

 > stopCluster(cl) 

これにて終了です。

パフォーマンス 試験1

それでは計算時間の短縮はどの程度になるのでしょうか。

Intel Core i7 (2.6GHz) 環境下で行った結果を示します。

for文による繰り返し処理

こちらが何の工夫も加えない、数式通りのアルゴリズムです。

f:id:usskaji:20131110131924p:plain

これを基準に以降の処理を見ていきます。

ベクトルとしてapply関数による処理

f:id:usskaji:20131110034123p:plain

Rが得意とするベクトル処理ですが、お勧めしません。このような関数の処理では最悪です。メモリを無駄に食う割に、全然お得感がしません。

2013.11.21 追記
Montecarlosさんよりコメントを頂きました。
apply系は内部でforを使った処理を行うため、
処理時間が余分にかかっているようです。

R言語の強みであるベクトルを単純に使った以下の処理では、
先のfor分による繰り返しよりも高速に計算が行えます。

> before <- proc.time()
> result <- sum(func_calc(c(1:N)))
> after <- proc.time()
> after - before
   ユーザ   システム       経過  
     0.198      0.092      0.290 

snowによる並列処理

ここでは6coreを用いた並列処理の結果を示します。

f:id:usskaji:20131110035713p:plain

と、apply関数よりは高速に処理されていることが分かりますが、数式通りのアルゴリズムとなるfor文と比べると二倍もの時間がかかっています。

パフォーマンス 試験2

そこで、上記のfor文による繰り返し処理をM=100回繰り返し行う処置について検討してみました。

上の結果より一度の計算に13秒かかることから、シングルコアでは20分程度の時間がかかると予想できます。この繰り返し処理をさらに繰り返す場合、並列化によって処理時間を短縮できるでしょう。

 M <- 100
func_step <- function(I){
    func_calc <- function(i){
        x <- (i - 0.5) / N
        y <- 4.0 / (1.0 + x * x)
        return(y/N)}
    N <- 10^7
    pi <- 0
    for(i in 1:N){
        pi <- pi + func_calc(i)}
    return(pi)}

 pi <- 0
 before <- proc.time();for(I in 1:M){pi <- pi + func_step(I)};after <- proc.time()
 print(after - before)

 pi <- 0
 before <- proc.time();pi<-sum(sapply(1:M, func_step);after <- proc.time()
 print(after - before)

pi <- 0
before <- proc.time();cl <- makeCluster(4, type="SOCK");clusterExport(cl);pi <- sum(parSapply(cl, 1:M, func_step));stopCluster(cl);after <- proc.time()
print(after - before)

処理結果は以下の通りです。

f:id:usskaji:20131110154840p:plain

"snow"パッケージを使った並列計算の結果、計算時間がおよそ3分の1になります。

処理に費やすコアを増やすことで、計算時間はさらに短縮できることでしょう。

f:id:usskaji:20131110154926p:plain

パフォーマンス 試験3

最後に、コア数と計算速度の関係を調べてみました。

TIME <- matrix(nrow=0, ncol=5)
for(CORE in 1:7){
    pi <- 0
    before <- proc.time();cl <- makeCluster(CORE, type="SOCK");clusterExport(cl, "I");pi <- sum(parSapply(cl, 1:M, func_step));stopCluster(cl);after <- proc.time()
    TIME <- rbind(TIME, after - before)
}

指定したコア数ごとに処理に要した時間は以下の通りです。

f:id:usskaji:20131110181515p:plain

今回の試験で用いたコードの場合、4coreまではコア数を増やすことで劇的な時間短縮がみられ、それ以降はコア数に対して線形に計算速度が増加していくことが分かりました。

結論

特定の繰り返し計算を行う場合には、"snow"パッケージが計算時間の短縮に大きく役に立ちます。試験1で行った計算についても、適切なコードに書き換えることで並列化による計算時間の短縮が行える可能性もあるでしょう。また、今回のコードに対しては、5core以上を使った並列処理による時間の短縮効果は限定的でのようです。占有するリソースと効果を見極めながら、最適な配列化を行いたいものです。

2013.11.21 追記
コメント欄でも述べましたが、
  N <- 10^10
とした場合、for文やfor文を内部に含むapply関数ではメモリ占有量が大きくなり、
処理が現実的ではなくなります。
そのような処理を行う場合には"snow"の恩恵を受けることができるでしょう。

参考

本記事は@antiplasticsさんの取り組みを参考に作成しました。

その他、"snow"の解説記事はこちら。


本ページには、Amazon.co.jpアソシエイト・プログラムによるリンクが含まれております。