R でコンパイルした関数をキャッシュする


compiler パッケージの cmpfun 関数を使うと関数をバイトコードにコンパイルすることができます。

library(compiler)

fibonacci <- function(n) {
   if (n <= 2) {
      1
   } else {
      Recall(n - 1) + Recall(n - 2)
   }
}
fibonacci_compiled <- cmpfun(fibonacci)

一般にバイトコードにコンパイルした関数は高速化されます[A]

コンパイルにより高速化の恩恵を得られる関数であれば常にコンパイルした状態で持っていたいのですが,コンパイルに時間がかかるため,スクリプト中で何回も呼ばれるならまだしも,スクリプト中ではほとんど呼ばれないけどスクリプト自体が何回も利用されるといった状況では逆に損をします。そこで,コンパイルした関数を RData ファイルに保存しておき, RData が存在すればそれを loadloadcmp するという手法が良いと思われます。

if (file.exists("fibonacci_compiled.RData")) {
   loadcmp("fibonacci_compiled.RData")
} else {
   fibonacci_compiled <- cmpfun(function(n) {
      if (n <= 2) {
         1
      } else {
         Recall(n - 1) + Recall(n - 2)
      }
   })
   save(fibonacci_compiled, file="fibonacci_compiled.RData")
}

しかしこれを毎度毎度記述するのは厄介なので,関数化します。

require(compiler)

cache <- function(name, f, file=sprintf("%s.RData", name), envir=parent.frame()) {
   src <- (function() attr(body(sys.function(1)), "srcfile"))()$filename
   if (file.exists(file) && (!file.exists(src) || file.info(file)$mtime > file.info(src)$mtime)) {
      loadcmp(file, envir=envir)
   } else {
      assign(name, cmpfun(f))
      assign(name, get(name), envir=envir)
      eval(parse(text=sprintf("save(%s, file=file)", name)))
   }
}

これを用いると,上の例を次のように書くことができます。

cache("fibonacci_compiled", function(n) {
   if (n <= 2) {
      1
   } else {
      Recall(n - 1) + Recall(n - 2)
   }
})

最初に呼ばれた時はコンパイルして RData を作成して保存し,二回目以降に呼ばれた場合は RData を読み込みます。

注意点として,このままだと関数定義を修正した場合に, RData が古い定義のままであるため,修正が反映されません。したがって関数定義を修正する度に RData を削除する必要があります[B][2013-04-12 22:40] ファイルを更新すると RData を再作成するように修正しました。

更新履歴

  • [2013-04-12 22:40] 呼び出し元ソースの変更を感知するように修正。
  • [2013-04-13 00:00] load ではなく loadcmp を使う様に修正。

脚注

  1. 上記の例だと,手元の環境では 2 倍程度速くなりました。ちなみに,コンパイルによる高速化とベクトル化による高速化は両立するとは限りません。 []
  2. ソースファイルと RData のタイムスタンプを比較して RData が古ければ更新,というのが自動化できれば良いのですが,呼び出し元のソースファイルのタイムスタンプを取得する方法がわかりません (ご存知の方がいれば教えてください)sys.function(1) で呼び出し元のソースファイルが取得できます。。ちなみにタイムスタンプは file.info 関数を使うと取得可能です。 []