ぶていのログでぶログ

思い出したが吉日

mruby-yyjsonを作った

前記事に続いてmrubyネタ yyjsonという高速でC言語から扱えるJSONライブラリがある。

github.com

mattn.kaoriya.net

これをmrubyから使えるようにしたmruby-yyjsonを作った

github.com

きっかけ

rfではmruby-jsonを使っている。 mruby-jsonの処理速度に不満があったわけではないのだが、2つ問題点があった。 1つ目はJSONのObjectに重複するキーがあるとinvalid jsonとなってしまう。 これは結構分かりづらいエラーで、私がこの問題にぶつかったときにgdbでデバッグするというくらいまで悩んだ問題だった。。 しかし、この問題はCRubyのjsonライブラリやjqでは発生しないので、この問題を解決したいと考えていた。

$ echo '{"foo":1,"foo":2}' | rf -j _
Error: invalid json

# CRubyではエラーにならない
$ echo '{"foo":1,"foo":2}' | ruby -rjson -e 'pp JSON.parse(STDIN.read)'
{"foo"=>2}

# jqでもエラーにならない
$ echo '{"foo":1,"foo":2}' | jq -c .
{"foo":2}

2つ目はエラーがわかりづらいこと。 先のエラーがinvalid jsonしかエラーに出なくてすごく大変だったのは書いた通り。

これらを解決しようとmruby-jsonのコードを読んでいたのだが、JSONパーサ自体に手をいれる必要がありそうに感じた(たぶん)のと、折角なら自作するかっとなりmruby-yyjsonを作った。

使い方

使い方は簡単でbuild_config.rbに conf.gem github: 'buty4649/mruby-yyjson'を書くだけ。

MRuby::Build.new do |conf|
-- snip --

  conf.gem github: 'buty4649/mruby-yyjson'
end

おわりに

mruby-yyjsonを作った。 yyjsonのAPIは非常にシンプルで使いやすいので、mruby bindingもかなりすんなり作れてよかった。 一通りは作ったと思うのでご活用ください。

おまけ: ベンチマーク

yyjsonは速さをウリにしているのでmruby-jsonとの比較ベンチを取ってみた。 ベンチマークのデータセットは https://github.com/ibireme/yyjson_benchmark にあるcanada.jsonとfgo.json。 前者はJSONベンチマーク界でスタンダードなファイル(?)なのと、後者は巨大で日本語が混じっていて面白そうだった*1ので選定した。

canada.json

$ hyperfine --input canada.json --warmup 3 './build/mruby-yyjson/bin/mruby -e "JSON.parse(STDIN.read)"' './build/mruby-json/bin/mruby -e "JSON.parse(STDIN.read)"'
Benchmark 1: ./build/mruby-yyjson/bin/mruby -e "JSON.parse(STDIN.read)"
  Time (mean ± σ):      11.7 ms ±   1.3 ms    [User: 6.5 ms, System: 3.2 ms]
  Range (min … max):     9.2 ms …  16.2 ms    222 runs

Benchmark 2: ./build/mruby-json/bin/mruby -e "JSON.parse(STDIN.read)"
  Time (mean ± σ):      36.9 ms ±   2.7 ms    [User: 29.1 ms, System: 6.5 ms]
  Range (min … max):    33.6 ms …  45.0 ms    69 runs

Summary
  ./build/mruby-yyjson/bin/mruby -e "JSON.parse(STDIN.read)" ran
    3.15 ± 0.41 times faster than ./build/mruby-json/bin/mruby -e "JSON.parse(STDIN.read)"

fgo.json

$ hyperfine --input fgo.json --warmup 3 './build/mruby-yyjson/bin/mruby -e "JSON.parse(STDIN.read)"' './build/mruby-json/bin/mruby -e "JSON.parse(STDIN.read)"'
Benchmark 1: ./build/mruby-yyjson/bin/mruby -e "JSON.parse(STDIN.read)"
  Time (mean ± σ):     717.1 ms ±  16.3 ms    [User: 633.5 ms, System: 120.3 ms]
  Range (min … max):   695.3 ms … 753.9 ms    10 runs

Benchmark 2: ./build/mruby-json/bin/mruby -e "JSON.parse(STDIN.read)"
  Time (mean ± σ):      1.067 s ±  0.052 s    [User: 0.969 s, System: 0.141 s]
  Range (min … max):    1.016 s …  1.183 s    10 runs

Summary
  ./build/mruby-yyjson/bin/mruby -e "JSON.parse(STDIN.read)" ran
    1.49 ± 0.08 times faster than ./build/mruby-json/bin/mruby -e "JSON.parse(STDIN.read)"

build_config.rb

MRuby::Build.new('mruby-yyjson') do |conf|
  conf.toolchain
  conf.cc.flags << '-O3'
  conf.gembox 'default'
  conf.gem File.expand_path( __dir__)
end

MRuby::Build.new('mruby-json') do |conf|
  conf.toolchain
  conf.cc.flags << '-O3'
  conf.gembox 'default'
  conf.gem mgem: 'mruby-json'
end

*1:Fate/Grand OrderのJSONらしいが公式のデータなのかはたまた…