RからGoの関数をつかう→はやい
これはなに
こんな話があります(だいぶ昔
で、RからRustを使おうというのが ゆたにサンの記事。*1
それで、この記事はRからGoを使おうって話です。
速度の比較をやりたいので、本記事ではフィボナッチ数列の関数を書いて比較します。
※この記事は別に「みんなGoを使おうぜ」みたいな趣旨ではないです。現状だとC++使うほうが応用範囲は広いと思うので
やりかた
RとGoの連携は実はもうやられていて、Rcppパッケージの開発などに携わっているRomain Francois氏のブログにやり方が書いてあります。
基本的な手順は
- Goでコードを書く
- soファイルを作成する
- CでGOの呼び出す関数を書く
- そのCをRから呼べるようにする
という感じです。結局Cなのね....
1. Goでコードを書く
./calc/main.go
という名前とします。ここでは3つの関数を定義しました。
package main import "C" //export DoubleIt func DoubleIt(x int) int { return x*2 ; } //export fib func fib(n int) int { if n <= 1 { return n } return fib(n-1) + fib(n-2) } //export fib_fast func fib_fast(n int) int { fn := make(map[int]int) for i := 0; i <= n; i++ { var f int if i <= 2 { f = 1 } else { f = fn[i-1] + fn[i-2] } fn[i] = f } return fn[n] } func main() {}
- 1個目・・・2をかけるだけの単純なコード
- 2個目・・・愚直に思いついたようにフィボナッチ数列を書いたやつ。
- 3個目・・・Go初心者ながらにいろいろ調べて速くしたやつ。
ここでのポイントはCでの呼び出しができるように import "C"
することと、関数定義の直前に //export 関数名
を入れることです。
2. soファイルを作成する
go build -o libcalc.so -buildmode=c-shared ./calc
ここで、 -o
オプションで付けるsoファイルの名前は lib で始まってれば何でもいいです。
3. CでGOの呼び出す関数を書く
これを ./rgo.c
とします。
#include <R.h> #include <Rinternals.h> extern int DoubleIt() ; extern int fib() ; extern int fib_fast() ; SEXP godouble(SEXP x){ return Rf_ScalarInteger( DoubleIt( INTEGER(x)[0] ) ) ; } SEXP gofib(SEXP x){ return Rf_ScalarInteger( fib( INTEGER(x)[0] ) ) ; } SEXP gofib_fast(SEXP x){ return Rf_ScalarInteger( fib_fast( INTEGER(x)[0] ) ) ; }
ポイントは、
- Rに関するヘッダーを書く
- Goの関数を引っ張ってくる(Goで
import "C"
したのはこのため) - Goの関数をラップするCの関数を書く。このとき出力の形式をRの出力に合うようにする(
Rf_ScalarInteger
の部分)
4. そのCをRから呼べるようにする
R CMD SHLIB
コマンドを使います
R CMD SHLIB -L. -lcalc rgo.c
このとき、ファイル名直前のオプションが -l
+ soファイル名からlibをとったやつ
となるようにするとうまくいきました。(この辺のルールがいまいち良く分かっていない.....)
これで rgo.so
が作成され、Rから(Cを介して)Goの関数が使えるようになりました。
使ってみる
dyn.load()
関数でsoファイルを読み込み、 .Call()
関数でCの関数を呼び出します。
dyn.load("rgo.so") .Call("godouble", 21L) #> [1] 42 .Call("gofib", 40L) #> [1] 102334155
速度比較
さあ、いよいよ本題。Goの関数とRの関数でフィボナッチ数列の速度を比較します。
R側の関数は以下です。
fibr_fast <- function(n){ if (n == 0) { return(0) } x <- numeric(max(n,2)) x[1] <- x[2] <- 1 m <- 3 while (m <= n){ x[m] <- x[m-1] + x[m-2] m <- m + 1 } x[n] }
速度比較用のコードは以下です。
compare <- microbenchmark::microbenchmark(Go = .Call("gofib_fast", 40L), R = fibr_fast(40L), times = 1000) # ブログ用出力 # knitr::kable(microbenchmark:::print.microbenchmark(compare)) compare microbenchmark:::autoplot.microbenchmark(compare)
Goのほうがちょっとだけ速くなりました。まあこのくらい単純なコードだとめっちゃさをつけるほうが難しいですが。
結論
たぶんものによってはGoのほうが速くなるんじゃないでしょうか。(マルチスレッド処理とか?)
あとGoの場合はなんでもかんでも速くなるというわけではなく、「速く動く書き方で書けば速く動く」みたいな感じかなとやってて思いました。(Goは再帰が苦手、とか)
そういえば
去年の UseR! でRomainがGoとRの連携するのを作る(ergoって名前だったっけ?)とかいってたけど、あれはどうなったんだろう。
今回のコード
今回のコードは全部、(遅くて速度検証用に使わなかったのも含めて)以下のGitHubに置いてます。 github.com
それから
今回Goで書いたコード、大きい数だと桁が溢れます。
package main func fib(n int) int { fn := make(map[int]int) for i := 0; i <= n; i++ { var f int if i <= 2 { f = 1 } else { f = fn[i-1] + fn[i-2] } fn[i] = f } return fn[n] } func main() { var x = fib(105) println(x) }
$ go run hoge.go -742723093263328478
この辺をちゃんとやるには以下の記事とかが参考になりそう。
*1:余談ですが、僕はこの記事に関する発表をTokyo.Rで見た覚えがあるんですが、もう3年以上前のことなのですね。。。光陰矢の如し。