Open layomia opened 3 years ago
Tagging subscribers to this area: @eiriktsarpalis, @layomia See info in area-owners.md if you want to be subscribed.
Author: | layomia |
---|---|
Assignees: | layomia |
Labels: | `area-System.Text.Json`, `tenet-performance` |
Milestone: | 7.0.0 |
From @mrange in https://github.com/dotnet/runtime/issues/57117:
Hi. I was asked if I had any further feedback on the JSON Serializer in ticket: #56995
One thing that came to mind is that in my experiments generating a deserialize method can lead to performance improvements.
I suspect you considered it and for some reason not implemented it but in case you are interested my testing shows an increase in performance with ~30% for a simple object when using generated code rather than metadata driven deserializer. In addition it seems to have positive impact on the memory aspect.
In case you are interested here is my experiment: https://github.com/mrange/T4JsonSerializer
Could be worth investigating IMHO but I am sure there subtleties to the deserialization problem that makes it hard to create a one size fits all generated version. Perhaps there is value to generate a version that works in many use cases but also allow the metadata driven version when flexibility is required.
From my testing the overhead when using a static deserializer fell significantly but as the JSON parsing process itself takes sometime the overall performance gain is not as good.
Consider honoring ReadCommentHandling
option - https://github.com/dotnet/runtime/issues/81131.
Quite often we have project requirements of consuming external API's where data is provided as JSON strings, so we don't have the luxury of serializing our own data for better downstream performance. Some community driven projects seem to indicate that there are further possibilities of improving deserialization performance from JSON string data.
Below are some benchmarks using very simple data. Some of these community driven projects have since been abandoned or don't handle new types so SystemTextJson is always the safest option but it would be nice to have improved performance.
BenchmarkDotNet=v0.13.5, OS=Windows 11 (10.0.22621.1265/22H2/2022Update/SunValley2) 12th Gen Intel Core i9-12900H, 1 CPU, 20 logical and 14 physical cores .NET SDK=7.0.103 [Host] : .NET 7.0.3 (7.0.323.6910), X64 RyuJIT AVX2 DefaultJob : .NET 7.0.3 (7.0.323.6910), X64 RyuJIT AVX2
Method | Mean | Error | StdDev | StdDev |
---|---|---|---|---|
SystemTextJson | 255.84 ns | 0.820 ns | 0.767 ns | 1.00 |
SystemTextJsonMetadataSrcGen | 258.11 ns | 0.389 ns | 0.325 ns | 1.01 |
SystemTextJsonDefaultSrcGen | 254.68 ns | 1.659 ns | 1.552 ns | 1.00 |
UTF8JSon | 131.54 ns | 0.800 ns | 0.748 ns | 0.51 |
UTF8JSon_Bytes | 116.64 ns | 0.277 ns | 0.246 ns | 0.46 |
SpanJSONUtf16 | 70.25 ns | 0.394 ns | 0.368 ns | 0.27 |
SpanJSONUtf8 | 94.89 ns | 0.641 ns | 0.599 ns | 0.37 |
SpanJSONUtf8_Bytes | 76.01 ns | 0.595 ns | 0.557 ns | 0.30 |
JsonSrcGen | 65.51 ns | 0.388 ns | 0.344 ns | 0.26 |
SourceGenTest.csproj:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net7.0</TargetFramework>
<ImplicitUsings>disable</ImplicitUsings>
<Nullable>disable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="BenchmarkDotNet" Version="0.13.5" />
<PackageReference Include="JsonSrcGen" Version="1.1.1" />
<PackageReference Include="SpanJson" Version="4.0.0" />
<PackageReference Include="Utf8Json" Version="1.3.7" />
</ItemGroup>
</Project>
Program.cs:
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using JsonSrcGen;
namespace SourceGenTest;
internal static class Program
{
static void Main(string[] args)
{
#if DEBUG
new JsonDeserializeCompare().DebugTest();
#else
var summary = BenchmarkRunner.Run<JsonDeserializeCompare>();
#endif
}
}
public class JsonDeserializeCompare
{
private string _jsonToDeserializeFromString;
private byte[] _jsonToDeserializeFromBytesUtf8;
private MetadataJsonClassContext _metadataJsonClassContext;
private DefaultJsonClassContext _defaultJsonClassContext;
private JsonSrcGenClass _jsonSrcGenClass;
private JsonSrcGen.JsonConverter _jsonSrcGenConverter;
public JsonDeserializeCompare()
{
_jsonToDeserializeFromString = "{\"Forename\":\"John\",\"Surname\":\"Smith\",\"Age\":42,\"Active\":true}";
_jsonToDeserializeFromBytesUtf8 = Encoding.UTF8.GetBytes(_jsonToDeserializeFromString);
_metadataJsonClassContext = new(new JsonSerializerOptions() { TypeInfoResolver = MetadataJsonClassContext.Default });
_defaultJsonClassContext = new(new JsonSerializerOptions() { TypeInfoResolver = DefaultJsonClassContext.Default });
_jsonSrcGenClass = new JsonSrcGenClass();
_jsonSrcGenConverter = new JsonSrcGen.JsonConverter();
}
public void DebugTest()
{
JsonClass jsonClass1 = SystemTextJson();
JsonClass jsonClass2 = SystemTextJsonMetadataSrcGen();
JsonClass jsonClass3 = SystemTextJsonDefaultSrcGen();
JsonClass jsonClass4 = UTF8JSon();
JsonClass jsonClass5 = UTF8JSon_Bytes();
JsonClass jsonClass6 = SpanJSONUtf16();
JsonClass jsonClass7 = SpanJSONUtf8();
JsonClass jsonClass8 = SpanJSONUtf8_Bytes();
JsonSrcGenClass jsonClass9 = JsonSrcGen();
//var reader = new Utf8JsonReader(_jsonToDeserializeUtf8, true, default);
//reader.Read();
}
[Benchmark(Baseline = true)]
public JsonClass SystemTextJson()
{
return System.Text.Json.JsonSerializer.Deserialize<JsonClass>(_jsonToDeserializeFromString);
}
[Benchmark]
public JsonClass SystemTextJsonMetadataSrcGen()
{
return System.Text.Json.JsonSerializer.Deserialize(_jsonToDeserializeFromString, _metadataJsonClassContext.JsonClass);
}
[Benchmark]
public JsonClass SystemTextJsonDefaultSrcGen()
{
return System.Text.Json.JsonSerializer.Deserialize(_jsonToDeserializeFromString, _defaultJsonClassContext.JsonClass);
}
[Benchmark]
public JsonClass UTF8JSon()
{
return Utf8Json.JsonSerializer.Deserialize<JsonClass>(Encoding.UTF8.GetBytes(_jsonToDeserializeFromString));
}
[Benchmark]
public JsonClass UTF8JSon_Bytes()
{
return Utf8Json.JsonSerializer.Deserialize<JsonClass>(_jsonToDeserializeFromBytesUtf8);
}
[Benchmark]
public JsonClass SpanJSONUtf16()
{
return SpanJson.JsonSerializer.Generic.Utf16.Deserialize<JsonClass>(_jsonToDeserializeFromString);
}
[Benchmark]
public JsonClass SpanJSONUtf8()
{
return SpanJson.JsonSerializer.Generic.Utf8.Deserialize<JsonClass>(Encoding.UTF8.GetBytes(_jsonToDeserializeFromString));
}
[Benchmark]
public JsonClass SpanJSONUtf8_Bytes()
{
return SpanJson.JsonSerializer.Generic.Utf8.Deserialize<JsonClass>(_jsonToDeserializeFromBytesUtf8);
}
[Benchmark]
public JsonSrcGenClass JsonSrcGen()
{
_jsonSrcGenConverter.FromJson(_jsonSrcGenClass, _jsonToDeserializeFromString);
return _jsonSrcGenClass;
}
}
public class JsonClass
{
public string Forename { get; set; }
public string Surname { get; set; }
public int Age { get; set; }
public bool Active { get; set; }
}
[JsonSrcGen.Json]
public class JsonSrcGenClass
{
public string Forename { get; set; }
public string Surname { get; set; }
public int Age { get; set; }
public bool Active { get; set; }
}
[JsonSourceGenerationOptions(PropertyNamingPolicy = JsonKnownNamingPolicy.Unspecified, GenerationMode = JsonSourceGenerationMode.Metadata)]
[JsonSerializable(typeof(JsonClass))]
internal partial class MetadataJsonClassContext : JsonSerializerContext
{
}
[JsonSourceGenerationOptions(PropertyNamingPolicy = JsonKnownNamingPolicy.Unspecified, GenerationMode = JsonSourceGenerationMode.Default)]
[JsonSerializable(typeof(JsonClass))]
internal partial class DefaultJsonClassContext : JsonSerializerContext
{
}
[JsonSourceGenerationOptions(PropertyNamingPolicy = JsonKnownNamingPolicy.Unspecified, GenerationMode = JsonSourceGenerationMode.Serialization)]
[JsonSerializable(typeof(JsonClass))]
internal partial class SerializationJsonClassContext : JsonSerializerContext
{
}
In https://github.com/dotnet/runtime/issues/51945, we addressed a mode in the JSON source generator that generates optimized serialization logic using
Utf8JsonWriter
directly. We should consider a similar mode for deserialization usingUtf8JsonReader
directly.