Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use instance methods for SerializeHandler in source gen #61448

Closed
Tracked by #63762
MichalStrehovsky opened this issue Nov 11, 2021 · 5 comments · Fixed by #72510
Closed
Tracked by #63762

Use instance methods for SerializeHandler in source gen #61448

MichalStrehovsky opened this issue Nov 11, 2021 · 5 comments · Fixed by #72510
Assignees
Labels
area-System.Text.Json source-generator Indicates an issue with a source generator feature tenet-performance Performance related issue
Milestone

Comments

@MichalStrehovsky
Copy link
Member

S.T.Json source generator generates static void *SerializeHandler(...) methods that perform serialization. These are invoked through a the SerializeHandler delegate on JsonObjectInfoValues<T>.

Invoking delegate to a static method is slower than invoking a delegate to an instance methods in .NET. Invocation of delegates to instance methods have comparable perf characteristics to a virtual call. Invocation of delegate to static methods goes through a shuffle thunk that deals with calling convention incompatibilities at the callsite (that assumes instance method) vs target (static method).

I ran the source generator, captured the outputs in *.cs files and changed this method:

            private static void JsonMessage2SerializeHandler(global::System.Text.Json.Utf8JsonWriter writer, global::JsonMessage2 value)
            {

                writer.WriteStartObject();
                writer.WriteString(PropName_message, value.message);

                writer.WriteEndObject();
            }

To not be static but be instance instead. Here's the difference when serializing a simple struct with a string field (the scenario used in TechEmpower benchmarks):

BenchmarkDotNet=v0.13.1, OS=Windows 10.0.19043.1348 (21H1/May2021Update)
Intel Core i7-4870HQ CPU 2.50GHz (Haswell), 1 CPU, 8 logical and 4 physical cores
.NET SDK=6.0.100-rc.2.21505.57
  [Host]   : .NET 6.0.0 (6.0.21.48005), X64 RyuJIT
  .NET 6.0 : .NET 6.0.0 (6.0.21.48005), X64 RyuJIT

Job=.NET 6.0  Runtime=.NET 6.0  
Method Mean Error StdDev
StaticMethod 102.89 ns 0.214 ns 0.189 ns
InstanceMethod 99.93 ns 0.337 ns 0.299 ns

I think the change needs to be done here, but I don't know how to test the source generators to ensure it's generating what I think it's generating, so I'm just filing an issue.

private static void {serializeMethodName}({Utf8JsonWriterTypeRef} {WriterVarName}, {valueTypeRef} {ValueVarName})
{{
{GetEarlyNullCheckSource(emitNullCheck)}
{serializeMethodBody}
}}";

@ghost
Copy link

ghost commented Nov 11, 2021

Tagging subscribers to this area: @dotnet/area-system-text-json
See info in area-owners.md if you want to be subscribed.

Issue Details

S.T.Json source generator generates static void *SerializeHandler(...) methods that perform serialization. These are invoked through a the SerializeHandler delegate on JsonObjectInfoValues<T>.

Invoking delegate to a static method is slower than invoking a delegate to an instance methods in .NET. Invocation of delegates to instance methods have comparable perf characteristics to a virtual call. Invocation of delegate to static methods goes through a shuffle thunk that deals with calling convention incompatibilities at the callsite (that assumes instance method) vs target (static method).

I ran the source generator, captured the outputs in *.cs files and changed this method:

            private static void JsonMessage2SerializeHandler(global::System.Text.Json.Utf8JsonWriter writer, global::JsonMessage2 value)
            {

                writer.WriteStartObject();
                writer.WriteString(PropName_message, value.message);

                writer.WriteEndObject();
            }

To not be static but be instance instead. Here's the difference when serializing a simple struct with a string field (the scenario used in TechEmpower benchmarks):

BenchmarkDotNet=v0.13.1, OS=Windows 10.0.19043.1348 (21H1/May2021Update)
Intel Core i7-4870HQ CPU 2.50GHz (Haswell), 1 CPU, 8 logical and 4 physical cores
.NET SDK=6.0.100-rc.2.21505.57
  [Host]   : .NET 6.0.0 (6.0.21.48005), X64 RyuJIT
  .NET 6.0 : .NET 6.0.0 (6.0.21.48005), X64 RyuJIT

Job=.NET 6.0  Runtime=.NET 6.0  
Method Mean Error StdDev
StaticMethod 102.89 ns 0.214 ns 0.189 ns
InstanceMethod 99.93 ns 0.337 ns 0.299 ns

I think the change needs to be done here, but I don't know how to test the source generators to ensure it's generating what I think it's generating, so I'm just filing an issue.

private static void {serializeMethodName}({Utf8JsonWriterTypeRef} {WriterVarName}, {valueTypeRef} {ValueVarName})
{{
{GetEarlyNullCheckSource(emitNullCheck)}
{serializeMethodBody}
}}";

Author: MichalStrehovsky
Assignees: -
Labels:

area-System.Text.Json, tenet-performance

Milestone: -

@dotnet-issue-labeler dotnet-issue-labeler bot added the untriaged New issue has not been triaged by the area owner label Nov 11, 2021
@eiriktsarpalis
Copy link
Member

It seems like a doable refactoring, I'd initially assumed that the JsonTypeInfo<T> properties were static, but apparently they are not.

Would it be possible to share your full benchmark code? The performance gains don't seem particularly impressive for what appears to be a very small POCO, so I don't know if performance gains provide sufficient motivation for us to go ahead with this refactoring. Note that delegate invocation is only used for the root-level object, the generated fast-path serialization methods generally compose using regular method calls:

        private static void FooSerializeHandler(global::System.Text.Json.Utf8JsonWriter writer, global::Foo? value)
        {
            if (value == null)
            {
                writer.WriteNullValue();
                return;
            }
        
            writer.WriteStartObject();
            writer.WritePropertyName(PropName_Bar);
            BarSerializeHandler(writer, value.Bar!);
        
            writer.WriteEndObject();
        }

cc @layomia

@MichalStrehovsky
Copy link
Member Author

Here's the benchmark: BdnJson.zip. The only difference between JsonMessage1 and JsonMessage2 is that one of them has an instance method as the handler.

The performance gains don't seem particularly impressive for what appears to be a very small POCO

FWIW, the POCO in use is the same one that is in use in TechEmpower benchmarks and we tend to be interested in the perf of those.

@layomia
Copy link
Contributor

layomia commented Nov 22, 2021

For completeness I'd like to first add a new variant of the JsonSerializer benchmarks (dotnet/performance#2153), then validate the change to instance methods against that.

@layomia layomia removed the untriaged New issue has not been triaged by the area owner label Nov 22, 2021
@layomia layomia added this to the 7.0.0 milestone Nov 22, 2021
@layomia layomia self-assigned this Nov 22, 2021
@eiriktsarpalis eiriktsarpalis added the source-generator Indicates an issue with a source generator feature label Jan 20, 2022
@jeffhandley jeffhandley modified the milestones: 7.0.0, Future Apr 6, 2022
@steveharter
Copy link
Member

For additional background on shuffle thunk see #45232 (comment)

I believe for every additional parameter over the count of 1 there will be a slight improvement.

MichalStrehovsky added a commit to MichalStrehovsky/runtime that referenced this issue Jul 20, 2022
@ghost ghost added the in-pr There is an active PR which will close this issue when it is merged label Jul 20, 2022
@ghost ghost removed the in-pr There is an active PR which will close this issue when it is merged label Jul 20, 2022
@ghost ghost locked as resolved and limited conversation to collaborators Aug 20, 2022
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
area-System.Text.Json source-generator Indicates an issue with a source generator feature tenet-performance Performance related issue
Projects
None yet
Development

Successfully merging a pull request may close this issue.

5 participants