★データ解析備忘録★

ゆる〜い技術メモ

RからGoの関数をつかう→はやい

これはなに

こんな話があります(だいぶ昔

qiita.com

qiita.com

で、RからRustを使おうというのが ゆたにサンの記事。*1

notchained.hatenablog.com

それで、この記事はRからGoを使おうって話です。

速度の比較をやりたいので、本記事ではフィボナッチ数列の関数を書いて比較します。

※この記事は別に「みんなGoを使おうぜ」みたいな趣旨ではないです。現状だとC++使うほうが応用範囲は広いと思うので

やりかた

RとGoの連携は実はもうやられていて、Rcppパッケージの開発などに携わっているRomain Francois氏のブログにやり方が書いてあります。

purrple.cat

基本的な手順は

  1. Goでコードを書く
  2. soファイルを作成する
  3. CでGOの呼び出す関数を書く
  4. その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)

f:id:songcunyouzai:20190520231613p:plain f:id:songcunyouzai:20190520231536p:plain

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年以上前のことなのですね。。。光陰矢の如し。