Rプログラミング入門 19回目
スピード
コードのスピード。
やっぱり、試行回数を増やしつつ、シミュレーションをするには、
コードが早くないとね。
Rには、早くするテクニックがあるよ。のお話し。
ベクトル化コード
コードを早くするには、下記3つを利用します。
- 論理テスト
- 添字操作
- 要素単位の実行
これらを使っているのがベクトル化コードと呼びます。
早いコードはこれ。ってのを実際に見て、速度を測った方が実感がわきます。
お題は、絶対値を取得する関数。
まずは、ループを使う方法。
abs_loop <- function(vec){ for(i in 1:length(vec)){ if(vec[i] < 0){ vec[i] <- -vec[i] } } vec }
次に、ベクトル化コード。
abs_set <- function(vec){ negs <- vec <- 0 vec[negs] <- vec[negs] * -1 vec }
ループを使わずに、論理テストと添字操作を使い、
負の要素に、-1を掛けています。
このlongオブジェクトを使って、それぞれの速度を計測してみます。
long <- rep(c(-1,1),5000000)
時間を計測する関数が、system.time関数。
まずは、ループを使う関数。
> system.time(abs_loop(long)) ユーザ システム 経過 11.37 0.05 11.66
単位は秒です。11秒かかります。
次に、ベクトル化コード。
> system.time(abs_set(long)) ユーザ システム 経過 0 0 0
0秒でした。
当然ながら、ループを使って1行ずつ処理するより
論理テストを利用して、該当する行をまとめて処理した方が早いです。
ベクトル化コードの書き方
この2点に気を付けます。
- 順次的なステップを実行するために、ベクトル化関数を使う
- 並列するケースの処理は、論理添え字を使う。ケースに該当する行全てを操作する
なので、こんな香ばしい関数があった場合、
change_symbols <- function(vec){ for(i in 1:length(vec)){ if(vec[i] == "DD"){ vec[i] = "joker" } else if(vec[i] == "C") { vec[i] = "ace"} else if(vec[i] == "7") { vec[i] = "king"} else if(vec[i] == "B") { vec[i] = "queen"} else if(vec[i] == "BB") { vec[i] = "jack"} else if(vec[i] == "BBB"){ vec[i] = "ten"} else { vec[i] = "nine"} } vec }
この様に、論理添え字を使って、該当する行を全て操作するように変更します。
change_symbols2 <- function(vec){ vec[vec == "DD"] <- "joker" vec[vec == "C"] <- "ace" vec[vec == "7"] <- "king" vec[vec == "B"] <- "queen" vec[vec == "BB"] <- "jack" vec[vec == "BBB"] <- "ten" vec[vec == "0"] <- "nine" vec }
これだけで、関数の速度がこんなに違います。
> vec <- c("DD","C","7","B","BB","BBB","0") > many <- rep(vec, 1000000) > system.time(change_symbols(many)) ユーザ システム 経過 23.37 0.01 23.75 > system.time(change_symbols2(many)) ユーザ システム 経過 1.28 0.13 1.48
さらに、ベクトル化関数を使うように変更します。
(ルックアップテーブルを使う方法です。)
change_symbols3 <- function(vec){ tb <- c("DD" = "joker","7" = "ace", "7" = "king","B" = "queen", "BB" = "jack","BBB" = "ten","0" = "nine") unname(tb[vec]) }
関数の処理速度がさらに速くなります。
> system.time(change_symbols3(many)) ユーザ システム 経過 0.28 0.05 0.33
とまぁ、なるべくループと分岐を避ける形にすると速くなります。
他に関数を高速にするには、メモリ空間で行われている事を意識する必要があります。
こんな2つの関数があったとします。
loop <- function(){ output <- rep(NA,100000) for(i in 1:100000){ output[i] <- i + 1 } }
loop2 <- function(){ output <- NA for(i in 1:100000){ output[i] <- i + 1 } }
それぞれの処理時間はこちら。
> system.time(loop()) ユーザ システム 経過 0.11 0.00 0.11 > system.time(loop2()) ユーザ システム 経過 4.88 0.09 5.05
1つ目の方が、約46倍速いです。
> 5.05 / 0.11 [1] 45.90909
というのも、1つ目の方は、outputオブジェクトが生成された際に、
100000個の要素が入る場所を確保しているからです。
2つ目の方は、ループが回るたびに、outputオブジェクトの領域を確保する処理が
入るの遅くなってしまいます。
この辺は、特段Rだからってわけでもないですね。
このメモリ上の動きを確認する章の見出しが、
Rで高速なforループを書く方法になっていますが、違和感あるなぁ。
次回で、Rプログラミング入門は最終回になりますが
今までに作ってきたスロットマシーンをシミュレーションする関数を
ベクトル化コードにする目的と直し方を実践します。