R 2.13.0についてのポストを一部翻訳してみた

via http://dirk.eddelbuettel.com/blog/2011/04/12/#the_new_r_compiler_package
意味が通ってない気がするところは後で直すかもしれません。すみません。


2011年4月11日 新しいR2.13.0のcompilerパッケージについての最初の実験


R 2.13.0が明日リリースされます。新しいバージョンには、RコアメンバのLuke Tierneyによる新しいパッケージcompilerが含まれます。この極めてシンプルかつ直接的な名前のパッケージは、Lukeが何年もの間取り組んできたものです。NEWSファイルには以下のように記載されています。

  • compilerパッケージがついに標準パッケージとなりました。使い方を知るには?compiler::compileコマンドを利用してください。このパッケージはRのためのバイトコードコンパイラの実装です。コンパイラはデフォルトでは本リリースには用いられていません。baseパッケージやその他の推奨されるパッケージをコンパイルする方法については、'RInstallation and Administration Manual'を参照してください。



Rのbaseパッケージ、その他推奨パッケージにはcompilerは利用されていないということです。


R/Finance 2011に先立って開催されるRcppワークショップに向けてスライドを作っているときに、compilerの良い例題を思いつきました。昨年の夏と秋に、Radford NealはRのパフォーマンス改善のための詳細な改善案(possible patches)をdevelopment listに送ってくれました。
Some patches were integrated and some more discussion ensued. One strand was on the difference in parsing between normal parens and curly braces. In isolation, these seem too large, but (as I recall) this is due some things 'deep down' in the parser.


しかしながら、この問題については何人かから強い要望として挙げられていました。
And it just doesn't die as a post from last week demonstrated once more.
去年、Christian Robertはこの問題をいくつかの必要な機能群に切り分けてくれました。私は、僅かな高速化のためにコードの10%以上を書きかえるようなことに時間を費やすくらいなら素直にRcppを使う、とpostしました。


ですので、ここでcompilerパッケージがどれだけの仕事をしてくれるのか試してみたいと思います。出発点は昨年と同じです。明示的なforループの中に、1/(1+x)を計算する5つの変数(variant)があります。実際のコードは、これらを効率化するためにベクトル化をしてくれるということはありません。ただし、Christianの議論に従って、パーサによる速度差の発生について言及しておくことは無意味ではありません。
とても便利なrbenchmarkパッケージを使って計測したサマリーは以下です。

    > ## cf http://dirk.eddelbuettel.com/blog/2010/09/07#straight_curly_or_compiled
    > f <- function(n, x=1) for (i in 1:n) x=1/(1+x)
    > g <- function(n, x=1) for (i in 1:n) x=(1/(1+x))
    > h <- function(n, x=1) for (i in 1:n) x=(1+x)^(-1)
    > j <- function(n, x=1) for (i in 1:n) x={1/{1+x}}
    > k <- function(n, x=1) for (i in 1:n) x=1/{1+x}
    > ## now load some tools
    > library(rbenchmark)
    > ## now run the benchmark
    > N <- 1e6
    > benchmark(f(N,1), g(N,1), h(N,1), j(N,1), k(N,1),
    +           columns=c("test", "replications",
    +           "elapsed", "relative"),
    +           order="relative", replications=10)
         test replications elapsed relative
    5 k(N, 1)           10   9.764  1.00000
    1 f(N, 1)           10   9.998  1.02397
    4 j(N, 1)           10  11.019  1.12853
    2 g(N, 1)           10  11.822  1.21077
    3 h(N, 1)           10  14.560  1.49119



これはChristianの結果を真似たものです。{}を使っているk()関数が最も速いことがわかります。最も遅い関数h()は、相対的に49%遅くなっています(実時間では5秒ほど異なります)。一方、普通の書き方であるf()関数はとてもよく働いてくれることが分かります。


これらの関数をcomplierで処理するとどうなるでしょうか。cmpfun()関数を用いてコンパイル後の変数(variants)を生成してから利用してみます。

    > ## R 2.13.0 brings this toy
    > library(compiler)
    > lf <- cmpfun(f)
    > lg <- cmpfun(g)
    > lh <- cmpfun(h)
    > lj <- cmpfun(j)
    > lk <- cmpfun(k)
    > # now run the benchmark
    > N <- 1e6
    > benchmark(f(N,1), g(N,1), h(N,1), j(N,1), k(N,1),
    +           lf(N,1), lg(N,1), lh(N,1), lj(N,1), lk(N,1),
    +           columns=c("test", "replications",
    +           "elapsed", "relative"),
    +           order="relative", replications=10)
           test replications elapsed relative
    9  lj(N, 1)           10   2.971  1.00000
    10 lk(N, 1)           10   2.980  1.00303
    6  lf(N, 1)           10   2.998  1.00909
    7  lg(N, 1)           10   3.007  1.01212
    8  lh(N, 1)           10   4.024  1.35443
    1   f(N, 1)           10   9.479  3.19051
    5   k(N, 1)           10   9.526  3.20633
    4   j(N, 1)           10  10.775  3.62673
    2   g(N, 1)           10  11.299  3.80310
    3   h(N, 1)           10  14.049  4.72871



驚異的に速くなりました!修正はとても容易でした!使い方は簡単です、関数を用意し、コンパイルするだけです。たったそれだけで、限界と思っていたところを超えるスピードが手に入りました。さらに言えば、括弧の書き方の違いによる差が消えていることがわかります。明示的な指数をとっている関数は依然として遅いですが、これは余計な関数呼び出しが含まれてしまうためだろうと思われます。


新しいcompilerパッケージを活用できる場面は極めてたくさんありそうだと言えるでしょう。どういったケースに有効なのか(あるいは有効でないのか)について、さらに多くの人がテストしてくれることを望みます。最後に、Rcppについてもう一度触れておきたいと思います。compiledコードとbyte compiledコードの差を見ることができるでしょう。

    > ## now with Rcpp and C++
    > library(inline)
    > ## and define our version in C++
    > src <- 'int n = as<int>(ns);
    +         double x = as<double>(xs);
    +         for (int i=0; i<n; i++) x=1/(1+x);
    +         return wrap(x); '
    > l <- cxxfunction(signature(ns="integer",
    +                            xs="numeric"),
    +                  body=src, plugin="Rcpp")
    > ## now run the benchmark again
    > benchmark(f(N,1), g(N,1), h(N,1), j(N,1), k(N,1),
    +           l(N,1),
    +           lf(N,1), lg(N,1), lh(N,1), lj(N,1), lk(N,1),
    +           columns=c("test", "replications",
    +           "elapsed", "relative"),
    +           order="relative", replications=10)
           test replications elapsed relative
    6   l(N, 1)           10   0.120   1.0000
    11 lk(N, 1)           10   2.961  24.6750
    7  lf(N, 1)           10   3.128  26.0667
    8  lg(N, 1)           10   3.140  26.1667
    10 lj(N, 1)           10   3.161  26.3417
    9  lh(N, 1)           10   4.212  35.1000
    5   k(N, 1)           10   9.500  79.1667
    1   f(N, 1)           10   9.621  80.1750
    4   j(N, 1)           10  10.868  90.5667
    2   g(N, 1)           10  11.409  95.0750
    3   h(N, 1)           10  14.077 117.3083



Rcppは依然として80から最高120倍は高速です。byte compiledコードに対して、速度差は約25-foldです。ただし、これらの例は実際のRコードに比べて非常に単純なものなので、実際の高速化具合はcompilerパッケージ、Rcpp共にもう少し小さいものになるでしょう。


Before I close, two more public service announcements. First, if you use Ubuntu see this post by Michael on r-sig-debian announcing his implementation of a suggestion of mine: we now have R alpha/beta/rc builds via his Launchpad PPA. Last Friday, I had the current R-rc snapshot of R 2.13.0 on my Ubuntu box only about six hours after I (as Debian maintainer for R) uploaded the underlying new R-rc package build to Debian unstable. This will be nice for testing of upcoming releases. Second, as I mentioned, the Rcpp workshop on April 28 preceding R/Finance 2011 on April 29 and 30 still has a few slots available, as has the conference itself.