jackson-module-kogera 2.19.0-beta25にて、value classのシリアライズ / デシリアライズに関連する処理でMethodHandleを使うように変更しました。
この記事では、変更の効果が最低でどの程度かを確認した結果を紹介します。
value class関連処理をMethodHandle化する理由
Jacksonでvalue classを違和感なく処理できるようにするため、value class関連処理は多くのリフレクション呼び出しが必要であり、リフレクション呼び出しが連続することもしばしば有るためです。
例えばデシリアライズ時は、最低でも「プライマリコンストラクタ呼び出し -> box化呼び出し」と2回のリフレクション呼び出しが必要です。
プロパティが非nullの場合、更にunbox化呼び出しも追加で必要です。
シリアライズの場合も、box化呼び出しとunbox化で2回のリフレクション呼び出しが必要です。
これらはMethodHandle化することで多くの改善が期待できます。
ベンチマーク
以下のリポジトリを使って比較を行いました。
ベンチマーク内容
MethodHandleで効果的に高速化するためには、型を明示することが重要です。
jackson-module-kogera 2.19.0-beta25では、ラップされる型がInt, Long, String, (Java)UUIDのいずれかだった場合に関して、型を明示的に扱うことによる最適化を行っています。
よって、この最適化の有無を比較する必要が有ります。
また、ベンチマーク内容は、Jacksonによる処理の量をなるべく減らし、今回の高速化に関する効果が表れやすいようにする必要が有ります。
以上を考慮し、ベンチマーク内容は以下のようにしました。
- ベンチマークの入出力は基本的に1桁整数 + 最低限の
JSON構造 - ベンチマーク対象となる型は1プロパティのみ持たせる
- 検証対象とする
value classは、Intをラップしたもの(= 型明示による最適化有り)と、Shortをラップしたもの(= 型明示による最適化無し)
最適化の効果はvalue classの数が増えた分だけ高まるため、このベンチマークからは、value classに関する処理が最低でどれだけ改善されたかが分かります。
結果
8ad900dをまとめたものです。
スループット
比が1より大きいほど改善されています。
シリアライズ
10/12ケースで改善が確認できました。
最も基本的なケースであるunbox.IntBenchmark.wrapped(シンプルなvalue classをプロパティに持つクラス)に関しては、約10%のスループット向上を記録しました。
key.jsonKey.ShortBenchmark.benchmarkとunbox.ShortBenchmark.directに関しては劣化が確認されましたが、それぞれ1%未満であり、誤差の範疇です。
| 2.19.0-beta24 | 2.19.0-beta25 | 2.19.0-beta25 / 2.19.0-beta24 | |
|---|---|---|---|
| key.jsonKey.IntBenchmark.benchmark | 1777332.255594 | 1817286.155664 | 1.022479702 |
| key.jsonKey.ShortBenchmark.benchmark | 1831096.322124 | 1819773.928008 | 0.9938166038 |
| key.unbox.IntBenchmark.benchmark | 1548940.949888 | 1557133.079293 | 1.005288858 |
| key.unbox.ShortBenchmark.benchmark | 739703.867427 | 760804.578200 | 1.028525889 |
| jsonValue.IntBenchmark.direct | 2892679.151030 | 2912412.745928 | 1.006821909 |
| jsonValue.IntBenchmark.wrapped | 1663825.635318 | 1866756.436996 | 1.121966387 |
| jsonValue.ShortBenchmark.direct | 2811182.009216 | 2904066.732081 | 1.033041163 |
| jsonValue.ShortBenchmark.wrapped | 1670737.423967 | 1832761.453181 | 1.096977554 |
| unbox.IntBenchmark.direct | 3179525.595621 | 3225851.588935 | 1.014570096 |
| unbox.IntBenchmark.wrapped | 1874000.549577 | 2057832.237266 | 1.098095856 |
| unbox.ShortBenchmark.direct | 3137818.450200 | 3135692.306827 | 0.9993224135 |
| unbox.ShortBenchmark.wrapped | 1631790.050848 | 1727636.426984 | 1.058736953 |
デシリアライズ
8/10ケースで改善が確認できました。
最も基本的なケースであるbyPrimaryConstructor.IntBenchmark.wrapped(シンプルなvalue classをプロパティに持つクラス)に関しては、約7%のスループット向上を記録しました。
byJsonCreator.ShortBenchmark.directとbyPrimaryConstructor.IntBenchmark.directに関しては、それぞれ約4%の劣化が確認されました。
ただし、これらはvalue classを直接デシリアライズする稀なケースです。
| 2.19.0-beta24 | 2.19.0-beta25 | 2.19.0-beta25 / 2.19.0-beta24 | |
|---|---|---|---|
| KeyBenchmark._int | 628928.592524 | 642907.263310 | 1.022226165 |
| KeyBenchmark._short | 536357.711456 | 555365.450802 | 1.03543855 |
| byJsonCreator.IntBenchmark.direct | 1784708.471187 | 1850169.260039 | 1.036678701 |
| byJsonCreator.IntBenchmark.wrapped | 916055.903594 | 958713.142722 | 1.046566196 |
| byJsonCreator.ShortBenchmark.direct | 1880281.625663 | 1809603.994811 | 0.9624111463 |
| byJsonCreator.ShortBenchmark.wrapped | 921136.345195 | 935274.268244 | 1.01534835 |
| byPrimaryConstructor.IntBenchmark.direct | 2147950.142659 | 2057745.453727 | 0.9580042911 |
| byPrimaryConstructor.IntBenchmark.wrapped | 995537.946778 | 1064727.840156 | 1.069500006 |
| byPrimaryConstructor.ShortBenchmark.direct | 1691792.929911 | 1739138.397988 | 1.02798538 |
| byPrimaryConstructor.ShortBenchmark.wrapped | 904488.697510 | 963217.354551 | 1.064930228 |
シングルショット
比が1より小さいほど改善されています。
シリアライズ
11/12ケースで劣化、劣化しなかったケースもほぼ差が有りませんでした。
最も基本的なケースであるunbox.IntBenchmark.wrappedに関して、劣化は2%未満でした。
| 2.19.0-beta24 | 2.19.0-beta25 | 2.19.0-beta25 / 2.19.0-beta24 | |
|---|---|---|---|
| key.jsonKey.IntBenchmark.benchmark | 184.561424 | 195.296498 | 1.058165318 |
| key.jsonKey.ShortBenchmark.benchmark | 187.364271 | 194.738718 | 1.039358875 |
| key.unbox.IntBenchmark.benchmark | 192.471979 | 192.413958 | 0.9996985483 |
| key.unbox.ShortBenchmark.benchmark | 185.425115 | 186.525809 | 1.005936057 |
| jsonValue.IntBenchmark.direct | 172.408964 | 176.969350 | 1.02645098 |
| jsonValue.IntBenchmark.wrapped | 286.545790 | 295.294399 | 1.030531277 |
| jsonValue.ShortBenchmark.direct | 175.135014 | 175.543538 | 1.002332623 |
| jsonValue.ShortBenchmark.wrapped | 282.984258 | 295.457164 | 1.044076325 |
| unbox.IntBenchmark.direct | 175.688984 | 177.968913 | 1.012977074 |
| unbox.IntBenchmark.wrapped | 292.981319 | 297.085654 | 1.014008862 |
| unbox.ShortBenchmark.direct | 176.957803 | 177.437413 | 1.002710307 |
| unbox.ShortBenchmark.wrapped | 281.099359 | 288.922695 | 1.027831213 |
デシリアライズ
7/10ケースで劣化、劣化しなかったケースもほぼ差が有りませんでした。
最も基本的なケースであるbyPrimaryConstructor.IntBenchmark.wrappedに関して、劣化は2%少しでした。
| 2.19.0-beta24 | 2.19.0-beta25 | 2.19.0-beta25 / 2.19.0-beta24 | |
|---|---|---|---|
| KeyBenchmark._int | 281.434946 | 280.577312 | 0.9969526386 |
| KeyBenchmark._short | 277.767897 | 286.769142 | 1.032405635 |
| byJsonCreator.IntBenchmark.direct | 269.644305 | 270.065927 | 1.001563623 |
| byJsonCreator.IntBenchmark.wrapped | 305.317364 | 307.809560 | 1.008162641 |
| byJsonCreator.ShortBenchmark.direct | 270.556615 | 275.870427 | 1.019640296 |
| byJsonCreator.ShortBenchmark.wrapped | 308.366190 | 306.834562 | 0.9950330871 |
| byPrimaryConstructor.IntBenchmark.direct | 275.885608 | 278.743274 | 1.010358155 |
| byPrimaryConstructor.IntBenchmark.wrapped | 302.336269 | 309.165245 | 1.022587353 |
| byPrimaryConstructor.ShortBenchmark.direct | 272.375393 | 269.656569 | 0.9900180998 |
| byPrimaryConstructor.ShortBenchmark.wrapped | 297.234643 | 316.185681 | 1.063757837 |
全体を通した考察
スループットに関しては、想定通り基本的なケース全般で改善を確認できました。
特にシリアライズは10%、デシリアライズは7%程度と、Jacksonに関するその他処理が有る中ではそれなりの改善量が得られたことは良かったです。
ただし、恐らくベンチマーク負荷が軽すぎたため、型明示による最適化有無の差はそれ程確認できませんでした(これに関しては、逆に「何が何でも最適化しなければならないほどの差は無い」とも言えるでしょうか)。
シングルショットに関しては、初期化処理の量自体は増えているため、想定通りほぼ劣化となりました。
ただ、全体を通しても劣化幅は10%未満であるため、それ程酷くならなかった点は安心しました。
data classとの差について
data classと比較した際のスコアは、残念ながらMethodHandle化後も大きいままでした。
補足
経験上、恐らくLambdaMetaFactoryを使った方が高速ですが、以下の理由からJacksonではLambdaMetaFactoryを使わない方針となっているため、試していません。
おまけ
ベンチマーク結果は以下に格納してあります。
一応比較用テンプレートも用意しているため、ローカルでの実行結果比較も可能です。