はじめに
System.Text.Jsonが、.NET 6.0よりSourceGeneratorによる、リフレクション無しのシリアライズをサポートするようになった。
そこで、従来のやり方であるリフレクションベースのやり方と、SourceGeneratorベースのやり方で、どの程度のパフォーマンスの違いが出るのか、検証してみた。
ソース生成のやり方
やり方自体は 公式ガイド もあるので詳しくは説明しないが、大まかにいうと
- シリアライズしたい型を作る
-
System.Text.Json.Serialization.JsonSerializerContext
を継承したpartialクラスを作る -
System.Text.Json.Serialization.JsonSerializableAttribute
を上記で作成したクラスに追加して、引数にシリアライズ対象の型をtypeofで渡す
とすると、JsonSerializerContextを継承したクラスに JsonTypeInfo<[シリアライズしたい型]> Default.[シリアライズしたい型]
というものが追加されるので、
それを JsonSerializer.Serialize<T>
や JsonSerializer.Deserialize<T>
に引数として渡せばOK。
結果
BenchmarkDotNetで計測した結果を以下に記述する。なお、今回使用したソースはgistにアップロードしておいた。
結果に対する考察は後述。
シリアライズ(JsonSerializeBenchmark)
単純な型のシリアライズベンチで、SerializeCodeGen
がSourceGeneratorを使った方で、SerializeClassic
が従来のやり方で行ったもの
BenchmarkDotNet=v0.13.1, OS=Windows 10.0.19043.1348 (21H1/May2021Update)
Intel Core i7-9700 CPU 3.00GHz, 1 CPU, 8 logical and 8 physical cores
.NET SDK=6.0.100
[Host] : .NET 6.0.0 (6.0.21.52210), X64 RyuJIT
ShortRun : .NET 6.0.0 (6.0.21.52210), X64 RyuJIT
Job=ShortRun IterationCount=3 LaunchCount=1
WarmupCount=3
Method | Mean | Error | StdDev | Gen 0 | Allocated |
---|---|---|---|---|---|
SerializeCodeGen | 209.6 ns | 35.72 ns | 1.96 ns | 0.0484 | 304 B |
SerializeClassic | 275.4 ns | 11.96 ns | 0.66 ns | 0.0482 | 304 B |
デシリアライズ(JsonDeserializeBenchmark)
JSON文字列から単純な型のインスタンスを生成するベンチマーク。
BenchmarkDotNet=v0.13.1, OS=Windows 10.0.19043.1348 (21H1/May2021Update)
Intel Core i7-9700 CPU 3.00GHz, 1 CPU, 8 logical and 8 physical cores
.NET SDK=6.0.100
[Host] : .NET 6.0.0 (6.0.21.52210), X64 RyuJIT
ShortRun : .NET 6.0.0 (6.0.21.52210), X64 RyuJIT
Job=ShortRun IterationCount=3 LaunchCount=1
WarmupCount=3
Method | Mean | Error | StdDev | Gen 0 | Allocated |
---|---|---|---|---|---|
DeserializeCodeGen | 399.4 ns | 59.15 ns | 3.24 ns | 0.0114 | 72 B |
DeserializeClassic | 393.3 ns | 77.47 ns | 4.25 ns | 0.0114 | 72 B |
複雑な型
型の中に別の型が更に入れ子で入っていた場合のベンチマーク
BenchmarkDotNet=v0.13.1, OS=Windows 10.0.19043.1348 (21H1/May2021Update)
Intel Core i7-9700 CPU 3.00GHz, 1 CPU, 8 logical and 8 physical cores
.NET SDK=6.0.100
[Host] : .NET 6.0.0 (6.0.21.52210), X64 RyuJIT
ShortRun : .NET 6.0.0 (6.0.21.52210), X64 RyuJIT
Job=ShortRun IterationCount=3 LaunchCount=1
WarmupCount=3
Method | Mean | Error | StdDev | Gen 0 | Allocated |
---|---|---|---|---|---|
SerializeCodeGen | 303.6 ns | 75.45 ns | 4.14 ns | 0.0625 | 392 B |
SerializeClassic | 542.2 ns | 25.10 ns | 1.38 ns | 0.1173 | 736 B |
DeserializeCodeGen | 798.6 ns | 157.66 ns | 8.64 ns | 0.0877 | 552 B |
DeserializeClassic | 758.8 ns | 143.35 ns | 7.86 ns | 0.0877 | 552 B |
考察
シリアライズに関して言えば、ソース生成の方が高速と言えそうだが、意外な結果として、デシリアライズの方は従来方式の方が性能が良かった。
複雑な型になるほどこの傾向はより顕著になっていったと言えるので、単純にソース生成が最適解というわけではなさそう。
これに関して、生成されたソースを見ると、シリアライズの方は機械的にWriterのメソッドを並べるだけになっているのに対し、
デシリアライズの方は、 JsonMetadataServices.CreateValueInfo<T>
で作成しており、ここで作るJsonTypeInfoが、リフレクションベースで作っているものに比べて効率が悪い部分があるのではないかと思われる。
ループで複数回作ったり予めキャッシュを作らせるようにしても、特に結果は変わらなかった。
この辺りは、ソース生成だからと言っても、結局実装次第という話。
おわりに
デシリアライズの結果は意外だったが、ソース生成型の方は、パフォーマンスだけではなく、リフレクションレスという事自体が
そもそもかなり利点になっていると言えるので、iOS等、リフレクションに大幅な制限がかけられている領域ではかなり大きな助けになると思う。
また、net7.0で採用予定のNativeAOTでもリフレクションは限定的にしか使えないものになるので、将来的にもっと重要な役割を担うかもしれない。
デシリアライズの結果について、なぜそうなるかという事は宿題にする。
参考リンク
-
JsonSrcGen
- ソースジェネレーターベースのJSONシリアライザ
- 機能的には非常に限定されている(フィールドにサポートされている以外の型があると面倒)が、性能面では圧倒的
- 公式の実装