dotnet / runtime

.NET is a cross-platform runtime for cloud, mobile, desktop, and IoT apps.
https://docs.microsoft.com/dotnet/core/
MIT License
15.19k stars 4.72k forks source link

Consider fast-path deserialization logic in JSON source generator #55043

Open layomia opened 3 years ago

layomia commented 3 years ago

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 using Utf8JsonReader directly.

ghost commented 3 years ago

Tagging subscribers to this area: @eiriktsarpalis, @layomia See info in area-owners.md if you want to be subscribed.

Issue Details
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 using `Utf8JsonReader` directly.
Author: layomia
Assignees: layomia
Labels: `area-System.Text.Json`, `tenet-performance`
Milestone: 7.0.0
layomia commented 3 years ago

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

mrange commented 3 years ago

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.

layomia commented 1 year ago

Consider honoring ReadCommentHandling option - https://github.com/dotnet/runtime/issues/81131.

cirrusone commented 1 year ago

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
{
}