Mukku John Blog

取り組んでいること を つらつら と

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点に気を付けます。

  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プログラミング入門は最終回になりますが
今までに作ってきたスロットマシーンをシミュレーションする関数を
ベクトル化コードにする目的と直し方を実践します。