RubyやRailsを使ってJSON生成する際にパフォーマンスを追求する機会があり、折角の機会なのでベンチマークを取って、最もパフォーマンスに優れた方法を調べてみました。
今回比較してみたのは以下の方法です。
1. ActiveSupport::JSON.encode
2. Yajl::Encoder.encode
3. JSON.generate
4. Oj.dump
それぞれ以下のような配列をJSON化することにしました。
def build_data(index)
{
id: index,
author: "夏目漱石",
product: "吾輩は猫である",
description: "吾輩わがはいは猫である。名前はまだ無い。どこで生れたかとんと見当けんとうがつかぬ。何でも薄暗いじめじめした所でニャーニャー泣いていた事だけは記憶している。吾輩はここで始めて人間というものを見た。しかもあとで聞くとそれは書生という人間中で一番獰悪どうあくな種族であったそうだ。この書生というのは時々我々を捕つかまえて煮にて食うという話である。しかしその当時は何という考もなかったから別段恐しいとも思わなかった。",
published_at: Date.new(1905, 10, 6)
}
end
data = 5000.times.map { |i| build_data(i + 1) }
ハッシュ値を持った要素5000個を配列に持ち、これをそれぞれ50回JSONエンコードした時のパフォーマンスを計測しています。
Benchmark.bm do |x|
x.report do
50.times { ActiveSupport::JSON.encode(data) }
end
x.report do
50.times { Oj.dump(data) }
end
x.report do
50.times { JSON.generate(data) }
end
x.report do
50.times { Yajl::Encoder.encode(data) }
end
結果はOj > JSON > YAJL >>>> ActiveSupport
計測結果は次のようになりました。
type | user | system | total | real |
---|---|---|---|---|
ActiveSupport | 19.450000 | 2.160000 | 21.610000 | ( 24.657747) |
YAJIL | 5.070000 | 0.080000 | 5.150000 | ( 5.192346) |
JSON | 2.440000 | 0.010000 | 2.450000 | ( 2.482170) |
Oj | 1.560000 | 0.110000 | 1.670000 | ( 1.724278) |
あくまで一例ではありますが、今回の検証では「Oj > JSON > YAJL >>>> ActiveSupport」という順にパフォーマンスが高いことが分かりました。
Ojは「Optimized JSON」の略でパフォーマンス最適化されているのでイメージ通りですが、YAJLについてJSONモジュールより高速と謳われているにも関わらず、倍以上遅い結果に終わっているのが意外でした。
他の事例を調べてみたところ、データ量が多いとYAJLのほうが高速になることがあるものの、少量のデータでJSONのほうが早いケースもあり、「YAJLが早いらしい」という伝聞で使っているとかえってパフォーマンスが落ちる可能性があるので注意が必要です。
今回最も早かったOjですが、メモリ消費量もJSONよりも少なく、データ量が増えてもYAJLよりも依然として高速なことから、基本的にはOjを採用するというのが無難かつ効果的と言えるでしょう。
Railsでrender json: ~
をデフォルトのまま使うのには注意
RailaからJSONを返すレスポンスにおいて、render json:@user
というように、エンコードされていない配列やオブジェクトをそのままrenderしてしまいがちです。
この時@user.to_json
された結果が自動的で返されるのですが、Railsではto_json
メソッドがJSONモジュールではなく、ActiveSupportのJSONモジュールが使われるように設定されています。(厳密には特定の場合のみデフォルトのJSONモジュールが使用される)
ActiveSupport::JSONのパフォーマンスについては先ほど示したように、他と比べて大きくパフォーマンスが落ちるため、高いスループットを実現するにはActiveSupportを使うのを避けるのが懸命でしょう。
最も望ましいのはOj.dump
を使ってあらかじめエンコードされたものを返すことです。
シンプルなオブジェクトならHashからエンコードしよう
RailsでAPIサーバーを作る時、パフォーマンスで最もボトルネックになるのはJSONの生成箇所です。
jbuilder
はとても柔軟にJSONを生成できますが、もともとの性能に加えて、partialを使用すればするほどパフォーマンスはますます劣化します。
ActiveModel::Serializer
を使えばいくらか向上しますが、それでもハッシュ値をエンコードするよりも大分パフォーマンスが落ちます。
jbuilderやActiveModel::Serializerは複雑なJSONを直感的に生成できる点で優れていますが、シンプルなオブジェクトをそのままJSON化するのであれば、素直にOjを使うだけで大きなパフォーマンスアップにつながります。
Ojを使うよりもJSONの方が早いケースもある
OjでActiveSupport::TimeWithZoneのエンコードをオプション無しで行うと、フォーマットがおかしなことになるので、compatモードで動かさざるを得ないことがあります。
公式サイトのベンチマークではJSONモジュールより早いと謳っていますが、手元で実際に動かしてみたところ「Oj通常モード > JSON > Ojのcompatモード」という測定結果になりました。少なくとも必ずしもOjの方が早いというわけでもなさそうです。
多くのケースでOjが早いことには変わりはありませんが、場合によってはJSONのほうが早いこともあり、特にパーサーを変えずにそのまま使っても決して悪い選択ではないと言えるでしょう。