コード生成でGsonをMoshiより高速化する

高速化しました。ざっくり実装しただけなのでリリースはしていません それなりに有効そうなのでjcenterにリリースしました。*1

ベンチマークについて

まずスコアをみていただきましょう。dynamic gsonがGoogle Gson v2.6.2、static gsonが今回最適化したもの、moshiが Square Moshi v1.1.0 です。ベンチマーク環境はXperia Z4 / Android 5.0.2です。(2016/03/09 初版よりデータ構造を複雑にして結果を更新。最新データはStaticGsonのREADMEを参照してください。)

D/XXX     : start benchmarking Dynamic Gson
D/XXX     : Dynamic Gson in serialization: 449ms
D/XXX     : Dynamic Gson in deserialization: 387ms

D/XXX     : start benchmarking Static Gson
D/XXX     : Static Gson in serialization: 198ms
D/XXX     : Static Gson in deserialization: 233ms

D/XXX     : start benchmarking Moshi
D/XXX     : Moshi in serialization: 270ms
D/XXX     : Moshi in deserialization: 656ms

serializeもdeserializeもstatic gsonはdybamic gsonよりも高速ですし、またmoshiよりも高速です。というか、moshiのdeserializeはgsonより遅くてどうしたんだという感じですが。これは繰り返しとっても変わらないので、実際に高速化できていると思います。

ターゲットとなるクラスはなるべくリアルなものがいいので、 droidkaigi2016 から拝借して少し改変したSession.javaです。

StaticGson/Session.java at master · gfx/StaticGson · GitHub

ベンチマークコードはこちら。

StaticGson/MainActivity.java at master · gfx/StaticGson · GitHub

N = 3000 はXperia Z4でGCが起きないような値です。つまり上のスコアのときGCは起きていません。他の端末の場合、GCが起きないように調整してスコアを取らないと正確にはならないので注意してください。

なぜ高速なのか

Annotation ProcessingでモデルごとのJsonAdapterFactoryを生成しています。これは、Gsonがもともとリフレクションで行っていることを静的に行うようにしたものです。リフレクションは通常のフィールドアクセスと比較して非常に遅いので、リフレクションを使わないだけで該当箇所は数倍高速になります。ただし、文字列解析や文字列生成が高速になるわけではないので、現実的にはせいぜい数割スコアが上がる程度です。

また、コード生成の副作用としてProGuardで壊れることがなくなるというのもメリットです。ProGuardが壊すのはリフレクションを使ったコードだけです。

生成されるコードは次のようになります。これはまさに、GsonのストリーミングAPIを使って手書きでTypeAdapterを書くとこうなるだろう、というコードです。

// ... imports ...

/**
 * Generated by {@code StaticGsonProcessor}
 */
@Keep
public class Session_TypeAdapterFactory implements TypeAdapterFactory {
  @Override
  public <T> TypeAdapter<T> create(final Gson gson, final TypeToken<T> typeToken) {
    return (TypeAdapter<T>) new TypeAdapter<Session>() {
      @Override
      public void write(JsonWriter writer, Session value) throws IOException {
        writer.beginObject();
        writer.name("id");
        writer.value(value.id);
        writer.name("title");
        writer.value(value.title);
        writer.name("description");
        writer.value(value.description);
        writer.name("speakerId");
        writer.value(value.speakerId);
        writer.name("stime");
        writer.value(value.stime);
        writer.name("etime");
        writer.value(value.etime);
        writer.name("categoryId");
        writer.value(value.categoryId);
        writer.name("placeId");
        writer.value(value.placeId);
        writer.name("languageId");
        writer.value(value.languageId);
        writer.name("slideUrl");
        writer.value(value.slideUrl);
        writer.name("movieUrl");
        writer.value(value.movieUrl);
        writer.name("shareUrl");
        writer.value(value.shareUrl);
        writer.name("checked");
        writer.value(value.checked);
        writer.endObject();
      }

      @SuppressWarnings("unchecked")
      @Override
      public Session read(JsonReader reader) throws IOException {
        Session object = new Session();
        reader.beginObject();
        while (reader.hasNext()) {
          switch (reader.nextName()) {
            case "id":
            object.id = (int) reader.nextLong();
            break;
            case "title":
            object.title = reader.nextString();
            break;
            case "description":
            object.description = reader.nextString();
            break;
            case "speakerId":
            object.speakerId = reader.nextLong();
            break;
            case "stime":
            object.stime = reader.nextLong();
            break;
            case "etime":
            object.etime = reader.nextLong();
            break;
            case "categoryId":
            object.categoryId = reader.nextLong();
            break;
            case "placeId":
            object.placeId = reader.nextLong();
            break;
            case "languageId":
            object.languageId = reader.nextString();
            break;
            case "slideUrl":
            object.slideUrl = reader.nextString();
            break;
            case "movieUrl":
            object.movieUrl = reader.nextString();
            break;
            case "shareUrl":
            object.shareUrl = reader.nextString();
            break;
            case "checked":
            object.checked = reader.nextBoolean();
            break;
            default: break;
          }
        }
        reader.endObject();
        return (Session) object;
      }
    };
  }
}

*1:というか、互換性を失わない高速化なのでGson本体に入るのが望ましいと思います。まずGsonメンテナと交渉をしてみるつもりです。 | 2016/03/09 追記: 独立したライブラリのほうがいいだろう、ということだったのでこのまま育てることにしました。