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

概要を3行で

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

はじめに

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

参考:rubyのmecabバインディングnattoを使う

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

Rubyのみで処理する

ちょっと試してみましょう。青空文庫の家なき子(上)全文の中から、名詞のみを抽出してみます。

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

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

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

Rubyでの処理

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の関数をつかう → はやい
参考3: Golang で Shared Library を出力する。

Go側の準備

Goのmecabバインディングは色々ありますが、今回はgo-mecabを使いました。

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を使って、さっき出力したmecab.soに定義されている関数を呼び出します。

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関数の戻り値は、カンマ区切りの文字列になります。

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

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

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

展望とか

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


『 Go 』Article List
Category List

Eye Catch Image
Read More

Androidに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

AWSに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

Bitcoinに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

CentOSに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

dockerに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

GitHubに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

Goに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

Javaに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

JavaScriptに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

Laravelに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

Pythonに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

Rubyに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

Scalaに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

Swiftに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

Unityに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

Vue.jsに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

Wordpressに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

機械学習に関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。