Ruby + mecabが遅いのでGoを経由する


# 概要を3行で

- Ruby + nattoで、長い文章の中から**特定の品詞だけ**を取得しようと思った。
- しかし遅かったので、**Goで書いたshared objectを経由**させた。
- 約60万文字の処理において、**10倍の速度**が出た

# はじめに

Rubyで形態素解析をする場合は、mecabをnatto経由で使うのが定番だと思います。

[参考:rubyのmecabバインディングnattoを使う](http://qiita.com/k-shogo/items/0f8a98c52913c729c7eb)

natto自体はmecabのCバインディングをFFI経由で叩いているはずなので必要十分な速度が出ますが、解析結果を形態素ごとに文字列処理したりすると、当然、Rubyでの処理になってしまうので、決して速いとは言えません。解析する文章量によっては、かなりの処理コストになってしまう可能性があります。

# Rubyのみで処理する

ちょっと試してみましょう。青空文庫の[家なき子(上)](http://www.aozora.gr.jp/cards/001047/card42924.html)全文の中から、名詞のみを抽出してみます。

具体的には、名詞のみを配列に突っ込むという処理です。ファイルのテキストを一度に読むと超重いので、一行ずつ読み込みます。

### 読み込むテキストファイル

文字数: 159,397文字
行数: 1926行

## Rubyでの処理

```rb
require "natto"

words = []
nm = Natto::MeCab.new

File.foreach("ienaki.txt") do |line|
  nm.parse(line) do |word|
    words << word.surface if word.feature.include?('名詞')
  end
end
```

## 結果

timeコマンドで測定しました。

|　|time|
|---|---|
|real|0m1.541s|
|user|0m1.448s|
|sys|0m0.083s|


約1.5秒ですね。約16万文字あるのを考えると、そこまで遅いわけでもないです。

# Goを経由させる

次にGoで出力したShared Libraryを経由させてみます。以下を参考にしました。

[参考2: RubyからGoの関数をつかう → はやい](http://qiita.com/achm/items/679b3f3af2cf377f0f02)
[参考3: Golang で Shared Library を出力する。](http://qiita.com/yanolab/items/1e0dd7fd27f19f697285)

## Go側の準備

Goのmecabバインディングは色々ありますが、今回は[go-mecab](https://github.com/shogo82148/go-mecab)を使いました。

```go:macab.go
package main

// #include <stdlib.h>
import "C"
import (
  "strings"
  "github.com/shogo82148/go-mecab"
  "unsafe"
)

//export parse
func parse(str *C.char) *C.char {
  tagger, err := mecab.New(map[string]string{})
  if err != nil { panic(err) }
  defer tagger.Destroy()

  tagger.Parse("")

  gostr := C.GoString(str)
  array := make([]string, 0, 0)
  scanner := bufio.NewScanner(strings.NewReader(gostr))
  // 1行ずつ回す
  for scanner.Scan() {
    node, err := tagger.ParseToNode(scanner.Text())
    if err != nil { panic(err) }
    // nodeごとに回す
    for ; node != (mecab.Node{}); node = node.Next() {
      if strings.Contains(node.Feature(), "名詞") {
        array = append(array, node.Surface())
      }
    }
  }
  cstr := C.CString(strings.Join(array, ","))
  return cstr
}

func main() {
}
```

先ほどRubyで行ったものとほぼ同じ処理を実行する、`parse`関数を定義しました。

ビルドします。

```
go build -buildmode=c-shared -o mecab.so mecab.go
```

## Rubyでの処理

RubyでShared Libraryを使用するためのGem、[ffi](https://github.com/ffi/ffi)を使って、さっき出力した`mecab.so`に定義されている関数を呼び出します。

```rb
require "ffi"

module Mecab
  extend FFI::Library
  ffi_lib "mecab.so"
  attach_function :parse, [:string], :string
end

open("ienaki.txt") do |f|
  Mecab.parse(f.read)
end
```

なお、Mecab.parse関数の戻り値は、カンマ区切りの文字列になります。

```rb
print Mecab.parse("モビルスーツの性能の違いが、戦力の決定的差でないということを教えてやる")
# => モビルスーツ,性能,違い,戦力,決定的,差,こと
```

### 結果

|　|Ruby + Go|Ruby only|
|---|---|---|
|real|0m0.246s|0m1.541s|
|user|0m0.190s|0m1.448s|
|sys|0m0.056s|0m0.083s|


約**5〜6倍**の速度となりました。期待していたほどの速度差は出ませんでしたが、それなりには早くなりました。

# 考察

試しに、**テキスト量を4倍**にして同じ処理を行ってみました。

文字数: 637,588文字
行数: 7704行

|　|Ruby + Go|Ruby only|
|---|---|---|
|real|0m0.551s|0m5.686s|
|user|0m0.493s|0m5.555s|
|sys|0m0.065s|0m0.116s|

Rubyはちょうど4倍ほどの時間になりましたが、Goを経由した場合は元の2.5倍ほどしか掛かっていません。**速度差は約10倍**ほどになりました。
つまり、関数呼び出しや文字列の型変換などといった、形態素解析を行って名詞のみを抜き出すという処理以外の部分に、かなり時間がかかっているかもしれません。

あと、`mecab.go`の

```go
cstr := C.CString(strings.Join(array, ","))
```

の部分なんですけど、このcstrはGoのGCから外れるっぽいです。cstrは`parse`関数の戻り値として使っているので、deferでメモリの解放ができません(Go初心者なので、return後に実行させる方法がわかりません・・・)。当然RubyのGCにも入らないと思うので、Ruby側で明示的に解放してあげるのが安全かもしれないです。

# 展望とか

今回は掲載しませんでしたが、`parse`メソッドの並列処理版も作ってみました。並列処理自体は出来たのですが、そもそも自分がGoの文字列処理に慣れていないので、並列処理とかmecab使うとか以前の段階で詰まり気味です(複数行の文字列をに分割するとか)。並列処理でパフォーマンスアップに成功したら、また更新したいと思います。