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

Perf improvements for small or value-type POCOs #37976

Merged
merged 3 commits into from
Jun 18, 2020

Conversation

steveharter
Copy link
Member

@steveharter steveharter commented Jun 16, 2020

For a TechEmPower benchmark which uses a one-property struct, this shows a ~1.2x serialization improvement during serialization.

Note a value-type (struct) POCO is not common and should only be used when there are very few properties -- instead a POCO should be a reference-type (class).

Fixes #36635

Summary of changes:

  • Add a LRU cache before the dictionary access that returns the metadata for the root type being (de)serialized. This is the biggest savings for small POCOs; larger POCOs are not affected since the dictionary access becomes insignificant compared to the rest of the work. Also this LRU helps most when there is low concurrency (few threads) or the same type is being repeatedly (de)serialized.
  • In internal code, pass the value types using in to avoid unnecessary copies. The more serializable properties a value-type contains, the bigger the savings.
  • Optimize property writes to include the quotes and colon as suggested by @Tornhoof. This also allowed for some AggressiveInlining fast-path changes to since the code path is now internal and specific to this optimization.
  • Other AggressiveInlining changes. Both added and removed. The crossgen size of STJ.dll is now 15K smaller (1071K to 1056K).
  • Property lookup for cache misses (due to case-insensitivity or different property ordering across JSON payloads for a given Type) is faster since the length is now embedded into the ulong key avoiding calls to SequenceEquals() when the length is different. This doesn't affect the TechEmPower scenarios.
  • Other smaller misc changes.
Click to expand for benchmarks Note the items in the "Slower" section appear to be random differences on my machine.

Changes <= 2% are ignored.

summary:
better: 60, geomean: 1.081
worse: 6, geomean: 1.042
total diff: 66

Slower diff/base Base Median (ns) Diff Median (ns) Modality
System.Text.Json.Serialization.Tests.WriteJson.Serializ 1.06 399455.00 422447.04
System.Text.Json.Serialization.Tests.WriteJson.Serializ 1.05 415556.58 436781.25
System.Text.Json.Serialization.Tests.WriteJson.Serializ 1.04 421354.56 438918.92
System.Text.Json.Serialization.Tests.ReadJson.DeserializeFromUtf8Byte 1.04 60123.19 62395.95
System.Text.Json.Serialization.Tests.WriteJson<ImmutableDictionary<String, Strin 1.04 22739.64 23542.93
System.Text.Json.Serialization.Tests.WriteJson.Serializ 1.03 393763.91 405899.18
Faster base/diff Base Median (ns) Diff Median (ns) Modality
System.Text.Json.Serialization.Tests.ReadJson.DeserializeFromUtf8Bytes 1.27 110.28 86.93
System.Text.Json.Serialization.Tests.TechEmPower.SerializeWithCachedBufferAndWri 1.22 157.92 128.98
System.Text.Json.Serialization.Tests.WriteJson.SerializeToUtf8Bytes 1.21 136.89 112.73
System.Text.Json.Serialization.Tests.TechEmPower.SerializeWithCachedBuffer 1.20 168.65 140.95
System.Text.Json.Serialization.Tests.WriteJson<HashSet>.SerializeToUtf8B 1.19 6923.56 5839.37
System.Text.Json.Serialization.Tests.ReadJson.DeserializeFromString 1.18 153.13 130.14
System.Text.Json.Serialization.Tests.WriteJson.SerializeToString 1.16 151.17 129.87
System.Text.Json.Serialization.Tests.WriteJson.Seria 1.15 320.42 278.51
System.Text.Json.Serialization.Tests.ReadJson.Deseri 1.14 304.91 267.69
System.Text.Json.Serialization.Tests.ReadMissingAndCaseInsensitive.Cas 1.14 1303.16 1145.62
System.Text.Json.Serialization.Tests.WriteJson.Seria 1.13 237.91 209.73
System.Text.Json.Serialization.Tests.WriteJson.SerializeToString 1.12 576.65 514.93
System.Text.Json.Serialization.Tests.WriteJson.SerializeToStream 1.12 188.54 168.38
System.Text.Json.Serialization.Tests.WriteJson.SerializeToUtf8By 1.12 316.44 282.72
System.Text.Json.Serialization.Tests.WriteJson.Seria 1.11 261.98 235.15
System.Text.Json.Serialization.Tests.WriteJson.SerializeToStream 1.11 400.38 360.51
System.Text.Json.Serialization.Tests.WriteJson.SerializeToStream 1.11 443.86 400.77
System.Text.Json.Serialization.Tests.ReadJson.Deseri 1.10 361.03 326.98
System.Text.Json.Serialization.Tests.ReadJson.DeserializeFromStream 1.09 287.03 263.85
System.Text.Json.Serialization.Tests.WriteJson.Seria 1.08 436.69 403.47
System.Text.Json.Serialization.Tests.WriteJson.Serial 1.08 682.21 631.29
System.Text.Json.Serialization.Tests.WriteJson.SerializeToStream 1.08 858.27 794.58
System.Text.Json.Serialization.Tests.ReadJson.Deseri 1.08 532.88 493.99
System.Text.Json.Serialization.Tests.WriteJson.SerializeToUtf8Bytes 1.08 400.49 371.56
System.Text.Json.Serialization.Tests.WriteJson.SerializeToString 1.08 348.29 323.17
System.Text.Json.Serialization.Tests.WriteJson.Serial 1.08 641.82 595.65
System.Text.Json.Serialization.Tests.ReadJson.DeserializeFromUtf8Byt 1.07 481.45 448.85
System.Text.Json.Serialization.Tests.ReadJson.DeserializeFromUtf 1.07 416.76 388.67
System.Text.Json.Serialization.Tests.WriteJson.SerializeObjectPropert 1.07 11685.38 10908.28
System.Text.Json.Serialization.Tests.ReadJson.Deseria 1.07 1536.53 1435.71
System.Text.Json.Serialization.Tests.ReadJson.DeserializeFromString 1.07 611.60 571.62
System.Text.Json.Serialization.Tests.WriteJson.SerializeToString 1.07 825.48 774.76
System.Text.Json.Serialization.Tests.WriteJson.SerializeObjectProperty 1.06 240.80 226.22
System.Text.Json.Serialization.Tests.WriteJson.Serial 1.06 851.35 805.19
System.Text.Json.Serialization.Tests.WriteJson.SerializeToString 1.05 11071.73 10494.79
System.Text.Json.Serialization.Tests.ReadJson.DeserializeFromStream 1.05 798.38 760.46
System.Text.Json.Serialization.Tests.ReadJson.DeserializeFromStr 1.05 29917.50 28523.15
System.Text.Json.Serialization.Tests.WriteJson.SerializeObjectProperty 1.05 965.81 921.32
System.Text.Json.Serialization.Tests.WriteJson.SerializeToUtf8Bytes 1.05 773.55 738.47
System.Text.Json.Serialization.Tests.WriteJson.SerializeObjectPr 1.05 497.30 474.84
System.Text.Json.Serialization.Tests.ReadJson.Deseriali 1.05 306043.34 292656.13
System.Text.Json.Serialization.Tests.ReadJson.DeserializeFromStr 1.05 481.74 460.92
System.Text.Json.Serialization.Tests.ReadMissingAndCaseInsensitive.Mis 1.04 797.91 765.76
System.Text.Json.Serialization.Tests.WriteJson.Serial 1.04 751.29 721.58
System.Text.Json.Serialization.Tests.ReadJson<ImmutableSortedDictionary<String, 1.04 69278.46 66565.23
System.Text.Json.Serialization.Tests.WriteJson.SerializeToString 1.04 17390.87 16731.64
System.Text.Json.Serialization.Tests.ReadJson.Deseria 1.04 1067.42 1028.77
System.Text.Json.Serialization.Tests.ReadJson.DeserializeFromStream 1.04 62911.90 60661.09
System.Text.Json.Serialization.Tests.ReadMissingAndCaseInsensitive.Bas 1.04 1191.68 1149.92
System.Text.Json.Serialization.Tests.WriteJson<ImmutableSortedDictionary<String, 1.04 16905.74 16313.72
System.Text.Json.Serialization.Tests.WriteJson.SerializeToStream 1.04 10598.59 10235.46
System.Text.Json.Serialization.Tests.ReadJson.DeserializeFromString 1.03 63096.82 61013.44
System.Text.Json.Serialization.Tests.WriteJson.SerializeToUtf8Bytes 1.03 16751.03 16241.27
System.Text.Json.Serialization.Tests.ReadJson.DeserializeFromString 1.03 1176.87 1142.29
System.Text.Json.Serialization.Tests.WriteJson<HashSet>.SerializeToStrin 1.03 6308.08 6126.69
System.Text.Json.Serialization.Tests.ReadJson.Deseria 1.03 1154.10 1121.70
System.Text.Json.Serialization.Tests.ReadJson.DeserializeFromStr 1.03 693.37 674.69
System.Text.Json.Serialization.Tests.WriteJson.SerializeToUtf8By 1.03 15614.74 15212.41
System.Text.Json.Serialization.Tests.ReadMissingAndCaseInsensitive.Cas 1.03 1181.37 1152.01
System.Text.Json.Serialization.Tests.WriteJson.SerializeToStream 1.02 16290.03 15892.85
Click to expand for temporary TechEmpower benchmark

Summary: 1.22x faster (when using a cached buffer)

Before:

Method Mean Error StdDev Median Min Max Gen 0 Gen 1 Gen 2 Allocated
SerializeWithCachedBuffer 169.0 ns 1.15 ns 1.08 ns 169.0 ns 167.2 ns 170.5 ns 0.0227 - - 144 B
SerializeWithCachedBufferAndWriter 155.3 ns 1.19 ns 1.11 ns 155.5 ns 153.3 ns 157.3 ns 0.0038 - - 24 B
SerializeWithManualCode 116.4 ns 0.57 ns 0.48 ns 116.5 ns 115.6 ns 117.2 ns 0.0188 - - 120 B

After:

Method Mean Error StdDev Median Min Max Gen 0 Gen 1 Gen 2 Allocated
SerializeWithCachedBuffer 141.6 ns 2.33 ns 2.18 ns 141.2 ns 138.0 ns 144.8 ns 0.0227 - - 144 B
SerializeWithCachedBufferAndWriter 130.8 ns 1.50 ns 1.40 ns 130.9 ns 128.6 ns 133.0 ns 0.0037 - - 24 B
SerializeWithManualCode 112.1 ns 1.14 ns 1.07 ns 112.4 ns 110.5 ns 113.7 ns 0.0188 - - 120 B

Code:

using BenchmarkDotNet.Attributes;
using MicroBenchmarks;
using System.Buffers;
using System.Threading.Tasks;

namespace System.Text.Json.Serialization.Tests
{
    public struct JsonMessage
    {
        public string message { get; set; }
    }

    public class TechEmPower
    {
        private static readonly JsonSerializerOptions SerializerOptions = new JsonSerializerOptions();

        ArrayBufferWriter<byte> _bufferWriter = new ArrayBufferWriter<byte>(160 * 16);
        Utf8JsonWriter _utf8Jsonwriter;


        [GlobalSetup]
        public void Setup()
        {
            _utf8Jsonwriter = new Utf8JsonWriter(_bufferWriter, new JsonWriterOptions { SkipValidation = true });
        }

        [BenchmarkCategory(Categories.Libraries, Categories.JSON)]
        [Benchmark]
        public void SerializeWithCachedBuffer()
        {
            _bufferWriter.Clear();
            var utf8JsonWriter = new Utf8JsonWriter(_bufferWriter, new JsonWriterOptions { SkipValidation = true });
            JsonSerializer.Serialize<JsonMessage>(utf8JsonWriter, new JsonMessage { message = "Hello, World!" }, SerializerOptions);
        }

        [BenchmarkCategory(Categories.Libraries, Categories.JSON)]
        [Benchmark]
        public void SerializeWithCachedBufferAndWriter()
        {
            _bufferWriter.Clear();
            JsonSerializer.Serialize<JsonMessage>(_utf8Jsonwriter, new JsonMessage { message = "Hello, World!" }, SerializerOptions);
        }

        [BenchmarkCategory(Categories.Libraries, Categories.JSON)]
        [Benchmark]
        public void SerializeWithManualCode()
        {
            _bufferWriter.Clear();
            using (var utf8JsonWriter = new Utf8JsonWriter(_bufferWriter))
            {
                var message = new JsonMessage { message = "Hello, World!" };
                utf8JsonWriter.WriteStartObject();
                utf8JsonWriter.WriteString("message", message.message);
                utf8JsonWriter.WriteEndObject();
            }
        }
    }
}

@steveharter steveharter requested a review from layomia June 16, 2020 16:14
@steveharter steveharter requested a review from jozkee as a code owner June 16, 2020 16:14
@steveharter steveharter self-assigned this Jun 16, 2020
@@ -62,7 +62,8 @@ protected override void CreateCollection(ref Utf8JsonReader reader, ref ReadStac
return false;
}

if (!converter.TryWrite(writer, enumerator.Current, options, ref state))
object? element = enumerator.Current;
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note this is for consistency with other code. Also applies to another case of this later.

@@ -48,10 +49,10 @@ internal override bool OnTryRead(ref Utf8JsonReader reader, Type typeToConvert,
// Read method would have thrown if otherwise.
Debug.Assert(tokenType == JsonTokenType.PropertyName);

ReadOnlySpan<byte> unescapedPropertyName = JsonSerializer.GetPropertyName(ref state, ref reader, options);
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note that GetPropertyName is marked with AggressiveInline but LookupProperty is not (it used to be, but there became 4 references to it over time causing code bloat).

@steveharter steveharter changed the title Perf improvements for small POCOs or value-type POCOs Perf improvements for small or value-type POCOs Jun 16, 2020
@@ -193,7 +191,7 @@ public override bool ReadJsonAndSetMember(object obj, ref ReadStack state, ref U
}
else if (Converter.CanUseDirectReadOrWrite)
{
if (!(isNullToken && IgnoreDefaultValuesOnRead && Converter.CanBeNull))
if (!isNullToken || !IgnoreDefaultValuesOnRead || !Converter.CanBeNull)
Copy link
Member Author

@steveharter steveharter Jun 17, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed this for readability (and to make short-circuiting of || obvious).

Copy link
Contributor

@layomia layomia left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@layomia layomia added this to the 5.0.0 milestone Jun 18, 2020
@steveharter steveharter merged commit c5a4e41 into dotnet:master Jun 18, 2020
@steveharter steveharter deleted the TechEmPower branch June 18, 2020 20:52
@adamsitnik
Copy link
Member

@stephentoub Please excuse me for not providing the code review on time.

@steveharter The numbers look really good! Thank you for sharing all the benchmark results!

@ghost ghost locked as resolved and limited conversation to collaborators Dec 8, 2020
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Is it possible to optimize JSON serialization any further?
3 participants