JQ Blog

OJ Gem

はじめ

ウェブサービスをやっていく中にはAPIを取得することが多い。今みているmastodonではOJというJson parserを使っていたので調べてみた。

インストール

1
gem install oj

or

1
2
# in Gemfile
gem 'oj'

使い方

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
require 'oj'

h = { 'one' => 1, 'array' => [ true, false ] }
json = Oj.dump(h)

# json =
# {
#   "one":1,
#   "array":[
#     true,
#     false
#   ]
# }

h2 = Oj.load(json)
puts "Same? #{h == h2}"
# true

他のParserとの比較

  • benchmark-ipsでベンチマークを取る
  • 比較対象
    • JSON
    • yajl
    • oj

まずロカールにapiのコントローラーをよいする。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Api::JsonCompareController < ActionController::API
  def index
    json = {
      test1: 'test1',
      test2: 'test2',
      test3: 'test3',
      test4: 'test4',
      test5: 'test5',
      test6: 'test6',
      test7: 'test7',
      test8: 'test8',
      test9: 'test9',
      test10: 'test10'
    }
    render json: json, status: :ok
  end
end

それから比較するコードを書く。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class JsonCompare
  require 'uri'
  require 'net/http'
  require 'json'
  require 'yajl'
  require 'oj'
  require 'benchmark/ips'

  def compare
    uri = URI "http://localhost:3000/api/json_compare"
    response = Net::HTTP.get uri

    Benchmark.ips do |x|
      x.report("JSON.parse") { JSON.parse(response) }
      x.report("Yajl") { Yajl::Parser.new.parse(response) }
      x.report("Oj") { Oj.load(response) }
      x.compare!
    end
  end
end

結果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Warming up --------------------------------------
          JSON.parse     9.786k i/100ms
                Yajl    10.364k i/100ms
                  Oj    17.009k i/100ms
Calculating -------------------------------------
          JSON.parse     99.053k (± 3.2%) i/s -    499.086k in   5.043910s
                Yajl    107.150k (± 3.4%) i/s -    538.928k in   5.036016s
                  Oj    183.400k (± 3.0%) i/s -    918.486k in   5.012844s

Comparison:
                  Oj:   183399.8 i/s
                Yajl:   107149.6 i/s - 1.71x  slower
          JSON.parse:    99053.1 i/s - 1.85x  slower

=> #<Benchmark::IPS::Report:0x007fdef3b04440
 @data=nil,
 @entries=
  [#<Benchmark::IPS::Report::Entry:0x007fdef8c3cbf0 @iterations=499086, @label="JSON.parse", @measurement_cycle=9786, @microseconds=5043910.0, @show_total_time=true, @stats=#<Benchmark::IPS::Stats::SD:0x007fdef8c3cc68 @error=3164, @mean=99053.0983205518>>,
   #<Benchmark::IPS::Report::Entry:0x007fdef7a234a0 @iterations=538928, @label="Yajl", @measurement_cycle=10364, @microseconds=5036016.0, @show_total_time=true, @stats=#<Benchmark::IPS::Stats::SD:0x007fdef7a23568 @error=3672, @mean=107149.64088869188>>,
   #<Benchmark::IPS::Report::Entry:0x007fdef89e6c98 @iterations=918486, @label="Oj", @measurement_cycle=17009, @microseconds=5012844.0, @show_total_time=true, @stats=#<Benchmark::IPS::Stats::SD:0x007fdef89e6d10 @error=5584, @mean=183399.80361005384>>]>

OJYajlよりは1.7倍、JSONよりは1.8倍速かった。
Stringじゃなくてモデルにしたらどうなるだろう。
apiコントローラーのコードを変えてみる。

1
2
3
4
5
6
class Api::JsonCompareController < ActionController::API
  def index
    @users = User.order(:id)
    render json: @users, status: :ok
  end
end

ちなみにUser.count100
さっきの比較コードを実行してみると、

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Warming up --------------------------------------
          JSON.parse   253.000  i/100ms
                Yajl   259.000  i/100ms
                  Oj   387.000  i/100ms
Calculating -------------------------------------
          JSON.parse      2.529k (± 3.4%) i/s -     12.650k in   5.007160s
                Yajl      2.612k (± 2.6%) i/s -     13.209k in   5.061015s
                  Oj      3.952k (± 3.6%) i/s -     20.124k in   5.098328s

Comparison:
                  Oj:     3952.4 i/s
                Yajl:     2611.7 i/s - 1.51x  slower
          JSON.parse:     2529.5 i/s - 1.56x  slower

=> #<Benchmark::IPS::Report:0x007fdf594c4678
 @data=nil,
 @entries=
  [#<Benchmark::IPS::Report::Entry:0x007fdf5da291a0 @iterations=12650, @label="JSON.parse", @measurement_cycle=253, @microseconds=5007160.0, @show_total_time=true, @stats=#<Benchmark::IPS::Stats::SD:0x007fdf5da29268 @error=87, @mean=2529.463823054468>>,
   #<Benchmark::IPS::Report::Entry:0x007fdf5a59b8c0 @iterations=13209, @label="Yajl", @measurement_cycle=259, @microseconds=5061015.0, @show_total_time=true, @stats=#<Benchmark::IPS::Stats::SD:0x007fdf5a59ba28 @error=67, @mean=2611.693591724044>>,
   #<Benchmark::IPS::Report::Entry:0x007fdf5d821510 @iterations=20124, @label="Oj", @measurement_cycle=387, @microseconds=5098328.0, @show_total_time=true, @stats=#<Benchmark::IPS::Stats::SD:0x007fdf5d821588 @error=141, @mean=3952.373849831794>>]>

OJYajlJSONより1.5倍速いことがわかる。

まとめ

単純に速度のパフォーマンス側をみるとOJを使った方が良さそう!

追記

mastodonでのOJの使い方

  • emoji.jsonの変換
1
2
# app/lib/emoji.rb
data = Oj.load(File.open(Rails.root.join('lib', 'assets', 'emoji.json')))
  • serviceクラスでのpayload作成
1
2
3
4
5
6
7
8
9
10
11
12
# app/services/batched_remove_status_service.rb
@json_payloads = statuses.map { |s| [s.id, Oj.dump(event: :delete, payload: s.id)] }.to_h

# app/services/fan_out_on_write_service.rb
@payload = InlineRenderer.render(status, nil, :status)
@payload = Oj.dump(event: :update, payload: @payload)

# app/services/notify_service.rb
Redis.current.publish("timeline:#{@recipient.id}", Oj.dump(event: :notification, payload: InlineRenderer.render(@notification, @recipient, :notification)))

# app/services/remove_status_service.rb
@payload = Oj.dump(event: :delete, payload: status.id)
  • reactコンポーネントへのprops
1
2
3
4
5
// app/views/home/index.html.haml
.app-holder#mastodon{ data: { props: Oj.dump(default_props) } }

// app/views/about/show.html.haml
#mastodon-timeline{ data: { props: Oj.dump(default_props) } }

参考

RubyのJSONパーサーのパース速度比較
OJ Github