Rでの並列処理の方法
概要
Rを速度のために使う言語ではないが、速さが必要な時も確かにある。コードをきれいによく書いても、あまりにも時間がかかるなら、普通は並列処理やGPUを使うことになる。一見するとRで並列処理をする必要があるのかと思うかもしれないが、ビッグデータを扱ったり、大規模なシミュレーションをする場合、並列処理は特に有用な手段になる。むしろRは本当に多くの並列処理を使用するとも見ることができる。
コード
以下は、1000個の大きな数を抽出して素因数分解をして、その時間を計るRのコードだ:
library(foreach)
library(doParallel)
eratosthenes<-function(n){
residue<-2:n
while(n %in% residue){
p<-residue[1]
residue<-residue[as.logical(residue%%p)]
}
return(p)
}
set.seed(150421)
test<-sample(2*1:10^5+1,1000)
system.time({
for(n in test){
eratosthenes(n)
}
})
numCores <- detectCores() -1
myCluster <- makeCluster(numCores)
registerDoParallel(myCluster)
record<-numeric(0)
clusterExport(myCluster, "record")
system.time({
foreach(n = test, .combine = c) %dopar% {
eratosthenes(n)
}
})
stopCluster(myCluster)
説明
素因数分解の場合、並列処理の利点を十分に活かすタイプの問題ではないが、それにもかかわらず、時間を半分以上短縮したことがわかる。論理プロセッサが8個あるからと言って、実行時間が1/8に減るわけではない。肯定的に言えば、並列処理をしなくてもCPUはもともと効率的に仕事を分配しているからだ。否定的に言えば、並列処理をしても使っているCPUは変わらないためだ。もちろん、肯定的であれ否定的であれ、並列処理がもたらす利点は相当なものだ。次は、ただのループを実行したときと並列処理をしたときのCPU使用率をGIFで示したものである:
上のGIFは、ループを実行したときのCPU使用率を示している。すべての論理プロセッサを使うわけではないが、基本的に仕事をするプロセッサだけが働き、それ以外は休む。
上のGIFは、並列処理を実行したときのCPU使用率を示している。通常のループと比べて、並列処理をすると、どうしてもすべてのプロセッサが100%の使用率を示すことがわかる。これが実際に実行時間をどれだけ短縮しても、最善を尽くしたことに変わりはない。
使用されるパッケージはforeach
とdoParallel
で、次のような関数を使用する:
detectCores()
: 現在使用しているコンピュータの論理プロセッサがいくつあるかを見つけて返す。例のコードで-1
でコアを一つ抜く理由は、並列処理にすべてのプロセッサを割り当ててしまった場合、コンピュータを使用することが不可能になるからだ。仕事をしているコンピュータで多くのことをしようとは思わないが、現実的には、最低限マウスカーソルを動かしたり、進行状況をある程度確認できる方が良い。だから、極端な効率を追求しない限り、プロセッサ一つくらいは余裕を持たせる。これをしないと、画面が全く動かないときに、ただ計算が多くてもたついているのか、コンピュータ自体がフリーズして動かないのかがわからない。makeCluster()
: 名前の通り、クラスタを作る関数だ。ただのメモリを割り当てる程度の概念として受け取っても構わない。registerDoParallel()
: 作成されたクラスタで並列処理ができるように割り当てる関数だ。clusterExport()
: クラスタで並列処理して得られたデータを受け取る変数を指定する。例の場合mycluster
で得られたデータをrecord
変数に割り当てるようにする。文法的には奇妙だが、慣れれば特に変なこともない。foreach()
: RやPython以外の言語ではよく見られるが、並列処理をするときは少し違う文法を持つ。n = test
はRの文法で見ればn in test
と似た役割を持つ。combine
オプションは、そうして得られたデータをどう集約するかを決定する。combine=c
は計算されるがままに保存する方式だ。c()
が元々ベクターを作る関数だからだ。c()
を使わずに順番に保存されるオプションなども選ぶことができるが、この場合性能低下を招くので、できるだけ最初からそういうコードを書かない方が良い。%dopar%
: 並列処理をするときには、ただの文法的な要素として考えるのが良い。foreach()
で定めたループをどのように実行するかを定める関数として十分だ。stopCluster()
: クラスタを停止する関数だ。makeCluster()
がメモリを割り当てるなら、stopCluster()
は解放する役割を持つ。コンピュータをある程度理解していれば、これがどれほど重要かは難しくないだろう。理解していなくても、並列処理をしなければならないほどの仕事をしているなら、このような関数がなぜあるかは容易に納得できるだろう。
面白い事実は、これらの関数が正確にどのような役割を持っているのか知らなくても大丈夫ということだ。ただコピペして、必要な部分だけを変え、foreach()
関数のオプションを正しく把握すればいい。百回読むより一回実行する方が良い。本当に必要な立場なら、関数一つ一つに意味を置かずに実行してみよう。