Skip to content

Commit

Permalink
improve performance of nullables
Browse files Browse the repository at this point in the history
  • Loading branch information
dbolin committed Aug 21, 2019
1 parent 3f6b6fb commit 598d2ab
Show file tree
Hide file tree
Showing 7 changed files with 161 additions and 17 deletions.
74 changes: 73 additions & 1 deletion Apex.Serialization/Internal/DynamicCode.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
{
Expand Down Expand Up @@ -331,6 +338,36 @@ private static Expression WriteValue(ParameterExpression stream, ParameterExpres
}
}

private static Expression? HandleNullableWrite(ParameterExpression stream, ParameterExpression output,
Type declaredType, ImmutableSettings settings, ImmutableHashSet<Type> 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<TStream>.ReserveSizeMethodInfo, Expression.Constant(1)) : Expression.Empty(),
Expression.Call(stream, BinaryStreamMethods<TStream>.GenericMethods<byte>.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<TStream>.GenericMethods<byte>.WriteValueMethodInfo, Expression.Constant((byte)0))
);
}

private static Expression? HandleCustomWrite(ParameterExpression output, Type declaredType,
Expression valueAccessExpression, ImmutableSettings settings)
{
Expand Down Expand Up @@ -437,7 +474,7 @@ internal static T GenerateReadMethodImpl<T>(Type type, ImmutableSettings setting
}

private static List<Expression> GetReadStatementsForType(Type type, ImmutableSettings settings, ParameterExpression stream,
ParameterExpression output, Expression result,List<ParameterExpression> localVariables,
ParameterExpression output, Expression result, List<ParameterExpression> localVariables,
ImmutableHashSet<Type> visitedTypes, bool readMetadata = false,
bool reserveNeededSize = true)
{
Expand Down Expand Up @@ -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)
{
Expand Down Expand Up @@ -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<ParameterExpression> localVariables, ImmutableHashSet<Type> 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<TStream>.ReserveSizeMethodInfo, Expression.Constant(1)) : Expression.Empty(),
Expression.Condition(
Expression.Equal(Expression.Call(stream, BinaryStreamMethods<TStream>.GenericMethods<byte>.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)
)
);
}
}
}
13 changes: 13 additions & 0 deletions Apex.Serialization/Internal/Reflection/TypeFields.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<byte>();
return true;
}

if (type.IsValueType && fields.All(f => IsPrimitive(f.FieldType)))
{
if(fields.Count == 0)
Expand Down
31 changes: 31 additions & 0 deletions Benchmark/PerformanceSuite_NullableInts.cs
Original file line number Diff line number Diff line change
@@ -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<int?> _listInt = new List<int?>(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<List<int?>>();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

namespace Benchmark
{
public class PerformanceSuite_Nullables : PerformanceSuiteBase
public class PerformanceSuite_NullableWrappedStruct : PerformanceSuiteBase
{
[StructLayout(LayoutKind.Explicit)]
public struct Struct1
Expand All @@ -24,21 +24,22 @@ public class Wrapper
public Struct1? NullableField;
}

private readonly List<Wrapper> _emptyListFull = new List<Wrapper>(Enumerable.Range(0, 1024).Select(x => new Wrapper()));
public PerformanceSuite_Nullables()
private readonly List<Wrapper> _listWrapper = new List<Wrapper>(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<List<Wrapper>>();
}
Expand Down
14 changes: 9 additions & 5 deletions BenchmarkCases/PerformanceSuite.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
25 changes: 25 additions & 0 deletions Tests/Apex.Serialization.Tests/ArrayTests.cs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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<CustomProperty>? Cu { get; }

public TestObject1(
ImmutableArray<CustomProperty>? 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()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}

Expand Down

0 comments on commit 598d2ab

Please sign in to comment.