Jackson でハイパフォーマンスな JSON 処理を追求する (第十七回 #渋谷java でお話してきました)

Java において JSON を読み書きするライブラリといえば、いまや Jackson がデファクトスタンダードかと思います。今回はこの Jackson をハイパフォーマンスに扱う方法について、第十七回 #渋谷java で発表してきました (発表資料はこのエントリの最後に掲載しています)。

About “Jackson”

Jackson について (それなりの経験がある Java エンジニアであれば釈迦に説法ですが) 簡単に説明すると、JSON を始めとして XML や YAML などのテキスト形式のデータフォーマットから Avro などのバイナリフォーマットにも対応した、データの読み込み・書き出しを実現するデータ処理ライブラリです。

mvnrepository.com の JSON Libraries ページ を見ると明らかなように、Gson などの他の JSON ライブラリよりも利用されることの多いライブラリであることが分かります。

Jackson の一般的な使い方については、以下の Qiita post によくまとまっているので、「Jackson の使い方がそもそもわからん!」という方はこちらをご覧いただくのをおすすめします。

Jackson を効率的に使う

Jackson は機能や API が充実しているがゆえ、一つのことをやるにもいく通りもの解決方法が存在し得ます。そのため、効率的に JSON を処理するには、入力データの形式や利用する API を慎重に選ぶ必要があります。

このあたりのベストプラクティスを知るには、下記の記事のうち最初の 2 つを読むのが手っ取り早いのですが、英語の記事を読むのが面倒という方は、最後の Qiita post からまず読み始めるのがよいでしょう。

上記記事を読むのすら面倒、という方は、とりあえず以下だけでも覚えておきましょう。

  • 入力の形式はできる限り byte[] もしくは InputStream, Reader とする。String は望ましくない
  • 出力の形式は OutputStremWriter とする。こちらもやはり、String は望ましくない
  • POJO と JSON の間の変換を必要とする処理は Data-binding の API (ObjectReaderObjectWriter, もしくは ObjectMapper) を利用し、オブジェクトは再利用する
  • POJO を扱う必要がなければ、Streaming API の利用も検討する (JsonParser, JsonGenerator)

Afterburner でさらに加速する

また Data-binding API を利用する場合 は、FasterXML が提供する Afterburner のモジュールを利用することで、JSON シリアライズ & デシリアライズ処理のパフォーマンスを向上させられます。

Afterburner の利用は簡単で、

<dependency>
  <groupId>com.fasterxml.jackson.module</groupId>
  <artifactId>jackson-module-afterburner</artifactId>
  <version>2.8.3</version>
</dependency>

(maven を利用している場合は) 上記の依存を pom.xml に追記した上で、ObjectMapper のオブジェクトを生成する際に

ObjectMapper mapper = new ObjectMapper()
  .registerModule(new AfterburnerModule());

とするだけです。このように、すでに Jackson を使っているプログラムがあったとしても、その実装を大幅に変更することなくごく僅かな変更だけで JSON 処理の高速化が期待できることに、この Afterburner の有用性を感じます。

Afterburner はその内部で ASM を利用し、本来リフレクションの API で実現されている各種処理 (POJO のコンストラクタや getter / setter の呼び出し) を ASM で生成したバイトコードでの処理に差し替えることで処理速度を稼いでいます。

モジュールとしての安定性も

Module is considered stable and has been used in production environments since version 2.2.

ということなので、速度にシビアな状況においては試してみる価値があるでしょう。

性能比較

さて、実際にベストプラクティスに従う書き方をした場合と、そうでない場合とでの性能を比較してみましょう。

ベンチマークは発表で使ったものと同じく、1,000 件の tweet が line delimited JSON として記録されているファイルをシーケンシャルに読み込み、”lang” 属性が “ja” (= 日本語 tweet) の件数を数え上げる、というものです。

ベンチマークのプログラムとその結果は以下のとおりです。

https://github.com/komiya-atsushi/java-playground/tree/master/jackson-performance

- スコア [ops/s]
ベースライン (非効率な実装) 2.596
ObjectMapper を再利用する 32.533
ObjectReader を利用する 33.306
ObjectReader#readValues() を利用する 43.641
Afterburner を利用する 49.764
Streaming API の利用に置き換える 77.969

非効率な実装のベースラインを除いてこの結果を評価をすると、

  • Data-binding API を利用する限りは、Afterburner まで利用して 1.5 倍ぐらいは速くできる
  • Streaming API を利用すれば、Data-binding API のときよりも大幅に速くできる

となります。

今回は JSON のデシリアライズのベンチマークだけでしたが、時間があるときにでも JSON シリアライズのベンチマークをとってみるつもりです。 また、Java における最速 JSON ライブラリは結局 Jackson なのか Boon なのか、いまいち釈然としない状況が僕の中で続いているので、何番煎じかわかりませんが Jackson vs Boon のベンチマークをとって比較してみようかと思います。

発表資料

最後になりますが、今回の #渋谷Java での発表資料はこちらです。