From 598d2ab910982fe0b781f0a740755c52bde5a819 Mon Sep 17 00:00:00 2001 From: dbolin Date: Tue, 20 Aug 2019 20:54:21 -0400 Subject: [PATCH] improve performance of nullables --- Apex.Serialization/Internal/DynamicCode.cs | 74 ++++++++++++++++++- .../Internal/Reflection/TypeFields.cs | 13 ++++ Benchmark/PerformanceSuite_NullableInts.cs | 31 ++++++++ ...PerformanceSuite_NullableWrappedStruct.cs} | 17 +++-- BenchmarkCases/PerformanceSuite.md | 14 ++-- Tests/Apex.Serialization.Tests/ArrayTests.cs | 25 +++++++ .../Collections/Objects/RandomHashcode.cs | 4 +- 7 files changed, 161 insertions(+), 17 deletions(-) create mode 100644 Benchmark/PerformanceSuite_NullableInts.cs rename Benchmark/{PerformanceSuite_Nullables.cs => PerformanceSuite_NullableWrappedStruct.cs} (59%) diff --git a/Apex.Serialization/Internal/DynamicCode.cs b/Apex.Serialization/Internal/DynamicCode.cs index aa12a7e..75f5a69 100644 --- a/Apex.Serialization/Internal/DynamicCode.cs +++ b/Apex.Serialization/Internal/DynamicCode.cs @@ -275,6 +275,13 @@ private static Expression WriteValue(ParameterExpression stream, ParameterExpres return primitiveExpression; } + var nullableExpression = HandleNullableWrite(stream, output, declaredType, settings, visitedTypes, valueAccessExpression); + if (nullableExpression != null) + { + inlineWrite = true; + return nullableExpression; + } + var customExpression = HandleCustomWrite(output, declaredType, valueAccessExpression, settings); if (customExpression != null) { @@ -331,6 +338,36 @@ private static Expression WriteValue(ParameterExpression stream, ParameterExpres } } + private static Expression? HandleNullableWrite(ParameterExpression stream, ParameterExpression output, + Type declaredType, ImmutableSettings settings, ImmutableHashSet visitedTypes, + Expression valueAccessExpression) + { + if (!declaredType.IsGenericType || declaredType.GetGenericTypeDefinition() != typeof(Nullable<>)) + { + return null; + } + + var hasValueMethod = declaredType.GetProperty("HasValue")!.GetGetMethod()!; + var valueMethod = declaredType.GetProperty("Value")!.GetGetMethod()!; + var nullableType = declaredType.GenericTypeArguments[0]; + var isPrimitive = TypeFields.IsPrimitive(nullableType); + + return Expression.IfThenElse( + Expression.Call(valueAccessExpression, hasValueMethod), + Expression.Block( + new[] { + !isPrimitive ? (Expression)Expression.Call(stream, BinaryStreamMethods.ReserveSizeMethodInfo, Expression.Constant(1)) : Expression.Empty(), + Expression.Call(stream, BinaryStreamMethods.GenericMethods.WriteValueMethodInfo, Expression.Constant((byte)1)), + } + .Concat( + GetWriteStatementsForType(nullableType, settings, stream, output, + Expression.Call(valueAccessExpression, valueMethod), false, Expression.Call(valueAccessExpression, valueMethod), + visitedTypes, writeSize: !isPrimitive)) + ), + Expression.Call(stream, BinaryStreamMethods.GenericMethods.WriteValueMethodInfo, Expression.Constant((byte)0)) + ); + } + private static Expression? HandleCustomWrite(ParameterExpression output, Type declaredType, Expression valueAccessExpression, ImmutableSettings settings) { @@ -437,7 +474,7 @@ internal static T GenerateReadMethodImpl(Type type, ImmutableSettings setting } private static List GetReadStatementsForType(Type type, ImmutableSettings settings, ParameterExpression stream, - ParameterExpression output, Expression result,List localVariables, + ParameterExpression output, Expression result, List localVariables, ImmutableHashSet visitedTypes, bool readMetadata = false, bool reserveNeededSize = true) { @@ -903,6 +940,13 @@ private static Expression ReadValue(ParameterExpression stream, ParameterExpress return primitiveExpression; } + var nullableExpression = HandleNullableRead(stream, output, declaredType, settings, localVariables, visitedTypes); + if (nullableExpression != null) + { + isInlineRead = true; + return nullableExpression; + } + var readStructExpression = ReadStructExpression(declaredType, stream, TypeFields.GetOrderedFields(declaredType)); if (readStructExpression != null) { @@ -971,5 +1015,33 @@ private static Expression ReadValue(ParameterExpression stream, ParameterExpress return null; } + + private static Expression? HandleNullableRead(ParameterExpression stream, ParameterExpression output, Type declaredType, + ImmutableSettings settings, List localVariables, ImmutableHashSet visitedTypes) + { + if (!declaredType.IsGenericType || declaredType.GetGenericTypeDefinition() != typeof(Nullable<>)) + { + return null; + } + + var nullableType = declaredType.GenericTypeArguments[0]; + var isPrimitive = TypeFields.IsPrimitive(nullableType); + var tempResult = Expression.Variable(nullableType, "tempResult"); + + return + Expression.Block( + !isPrimitive ? (Expression)Expression.Call(stream, BinaryStreamMethods.ReserveSizeMethodInfo, Expression.Constant(1)) : Expression.Empty(), + Expression.Condition( + Expression.Equal(Expression.Call(stream, BinaryStreamMethods.GenericMethods.ReadValueMethodInfo), Expression.Constant((byte)0)), + Expression.Default(declaredType), + Expression.Convert( + Expression.Block(new[] { tempResult }, + GetReadStatementsForType(nullableType, settings, stream, output, tempResult, localVariables, + visitedTypes, reserveNeededSize: !isPrimitive) + .Concat(new[] { tempResult })), + declaredType) + ) + ); + } } } diff --git a/Apex.Serialization/Internal/Reflection/TypeFields.cs b/Apex.Serialization/Internal/Reflection/TypeFields.cs index e67978c..9a42b35 100644 --- a/Apex.Serialization/Internal/Reflection/TypeFields.cs +++ b/Apex.Serialization/Internal/Reflection/TypeFields.cs @@ -99,6 +99,19 @@ private static bool TryGetSizeForStruct(Type type, out int sizeForField) var fields = GetFields(type); int size; + if (type.IsGenericType && typeof(Nullable<>) == type.GetGenericTypeDefinition()) + { + var (innerSize, isRef) = GetSizeForType(type.GenericTypeArguments[0]); + if(isRef) + { + sizeForField = 5; + return false; + } + + sizeForField = innerSize + Unsafe.SizeOf(); + return true; + } + if (type.IsValueType && fields.All(f => IsPrimitive(f.FieldType))) { if(fields.Count == 0) diff --git a/Benchmark/PerformanceSuite_NullableInts.cs b/Benchmark/PerformanceSuite_NullableInts.cs new file mode 100644 index 0000000..04383d4 --- /dev/null +++ b/Benchmark/PerformanceSuite_NullableInts.cs @@ -0,0 +1,31 @@ +using BenchmarkDotNet.Attributes; +using System.Collections.Generic; +using System.Linq; + +#nullable disable + +namespace Benchmark +{ + public class PerformanceSuite_NullableInts : PerformanceSuiteBase + { + private readonly List _listInt = new List(Enumerable.Range(0, 1024).Select(x => (int?)x)); + + public PerformanceSuite_NullableInts() + { + _listInt.Capacity = 1024; + S_NullableInts(); + } + + [Benchmark] + public void S_NullableInts() + { + Serialize(_listInt); + } + + [Benchmark] + public object D_NullableInts() + { + return Deserialize>(); + } + } +} diff --git a/Benchmark/PerformanceSuite_Nullables.cs b/Benchmark/PerformanceSuite_NullableWrappedStruct.cs similarity index 59% rename from Benchmark/PerformanceSuite_Nullables.cs rename to Benchmark/PerformanceSuite_NullableWrappedStruct.cs index 69b535c..f344549 100644 --- a/Benchmark/PerformanceSuite_Nullables.cs +++ b/Benchmark/PerformanceSuite_NullableWrappedStruct.cs @@ -8,7 +8,7 @@ namespace Benchmark { - public class PerformanceSuite_Nullables : PerformanceSuiteBase + public class PerformanceSuite_NullableWrappedStruct : PerformanceSuiteBase { [StructLayout(LayoutKind.Explicit)] public struct Struct1 @@ -24,21 +24,22 @@ public class Wrapper public Struct1? NullableField; } - private readonly List _emptyListFull = new List(Enumerable.Range(0, 1024).Select(x => new Wrapper())); - public PerformanceSuite_Nullables() + private readonly List _listWrapper = new List(Enumerable.Range(0, 1024).Select(x => new Wrapper())); + + public PerformanceSuite_NullableWrappedStruct() { - _emptyListFull.Capacity = 1024; - S_Nullables(); + _listWrapper.Capacity = 1024; + S_NullableWrapper(); } [Benchmark] - public void S_Nullables() + public void S_NullableWrapper() { - Serialize(_emptyListFull); + Serialize(_listWrapper); } [Benchmark] - public object D_Nullables() + public object D_NullableWrapper() { return Deserialize>(); } diff --git a/BenchmarkCases/PerformanceSuite.md b/BenchmarkCases/PerformanceSuite.md index 5af4068..28ce726 100644 --- a/BenchmarkCases/PerformanceSuite.md +++ b/BenchmarkCases/PerformanceSuite.md @@ -43,8 +43,12 @@ Internal benchmarks | S_SortedDictionaryOfValues | 1.351 us | 0.0048 us | 0.0045 us | - | - | - | - | | D_SortedDictionaryOfValues | 9.416 us | 0.1115 us | 0.0989 us | 0.4425 | - | - | 4936 B | -| Method | Mean | Error | StdDev | Gen 0 | Gen 1 | Gen 2 | Allocated | -|------------ |----------:|----------:|----------:|-------:|------:|------:|----------:| -| S_Nullables | 6.177 us | 0.0572 us | 0.0535 us | - | - | - | - | -| D_Nullables | 15.901 us | 0.1371 us | 0.1282 us | 3.9673 | - | - | 49208 B | - +| Method | Mean | Error | StdDev | Gen 0 | Gen 1 | Gen 2 | Allocated | +|--------------- |---------:|----------:|----------:|-------:|------:|------:|----------:| +| S_NullableInts | 3.582 us | 0.0100 us | 0.0093 us | - | - | - | - | +| D_NullableInts | 5.318 us | 0.1031 us | 0.1227 us | 0.6638 | - | - | 8248 B | + +| Method | Mean | Error | StdDev | Gen 0 | Gen 1 | Gen 2 | Allocated | +|------------------ |----------:|----------:|----------:|-------:|------:|------:|----------:| +| S_NullableWrapper | 3.540 us | 0.0141 us | 0.0125 us | - | - | - | - | +| D_NullableWrapper | 10.697 us | 0.0948 us | 0.0886 us | 3.9978 | - | - | 49208 B | diff --git a/Tests/Apex.Serialization.Tests/ArrayTests.cs b/Tests/Apex.Serialization.Tests/ArrayTests.cs index 875bee7..d959092 100644 --- a/Tests/Apex.Serialization.Tests/ArrayTests.cs +++ b/Tests/Apex.Serialization.Tests/ArrayTests.cs @@ -1,6 +1,7 @@ using FluentAssertions; using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.Linq; using System.Numerics; using System.Runtime.InteropServices; @@ -209,6 +210,30 @@ public void NullableDecimalArray() RoundTrip(x); } + public sealed class TestObject1 + { + public decimal O { get; } + public string? Im { get; } + public string? T { get; } + public string? In { get; } + public ImmutableArray? Cu { get; } + + public TestObject1( + ImmutableArray? cu) + { + Cu = cu; + } + } + + [Fact] + public void NestedNullable() + { + var a = Enumerable.Range(0, 100).Select(x => new CustomProperty("", new Value { _string = "" })).ToImmutableArray(); + var x = new TestObject1(a); + + RoundTrip(x, (a,b) => true); + } + [Fact] public void NullArrays() { diff --git a/Tests/Apex.Serialization.Tests/Collections/Objects/RandomHashcode.cs b/Tests/Apex.Serialization.Tests/Collections/Objects/RandomHashcode.cs index cfb0d64..750b73b 100644 --- a/Tests/Apex.Serialization.Tests/Collections/Objects/RandomHashcode.cs +++ b/Tests/Apex.Serialization.Tests/Collections/Objects/RandomHashcode.cs @@ -23,15 +23,13 @@ public override int GetHashCode() public static bool operator ==(RandomHashcode hashcode1, RandomHashcode hashcode2) => hashcode1.Equals(hashcode2); public static bool operator !=(RandomHashcode hashcode1, RandomHashcode hashcode2) => !(hashcode1 == hashcode2); - private static Random _random = new Random(); - internal static void NewRandomizer() { var old = HashCodeRandomizer; while (HashCodeRandomizer == old) { - HashCodeRandomizer = _random.Next(); + HashCodeRandomizer = HashCodeRandomizer * 37 + 19999; } }