Skip to content

Commit

Permalink
Add 'Read*' deserialize methods (#199)
Browse files Browse the repository at this point in the history
This heavily improves performance by avoiding generic virtual dispatch in the common case of deserializing primitive types inside a struct.

After these changes, the performance difference between Serde and S.T.J is almost entirely eliminated:

```
| Method      | Mean       | Error   | StdDev  | Gen0   | Gen1   | Allocated |
|------------ |-----------:|--------:|--------:|-------:|-------:|----------:|
| JsonNet     | 1,032.3 ns | 2.80 ns | 2.48 ns | 0.4063 | 0.0019 |    3400 B |
| SystemText  |   617.0 ns | 3.14 ns | 2.62 ns | 0.0534 |      - |     448 B |
| SerdeJson   |   633.4 ns | 1.44 ns | 1.21 ns | 0.0963 |      - |     808 B |
| SerdeManual |   632.7 ns | 4.01 ns | 3.13 ns | 0.0963 |      - |     808 B |
```
  • Loading branch information
agocke committed Sep 9, 2024
1 parent c3079e0 commit 8633dc0
Show file tree
Hide file tree
Showing 82 changed files with 556 additions and 636 deletions.
30 changes: 18 additions & 12 deletions perf/bench/SampleTypes.cs
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ public partial record LocationWrap : IDeserialize<Location>
"Location",
typeof(Location).GetCustomAttributesData(),
[
("id", StringWrap.SerdeInfo, typeof(Location).GetProperty("Id")!),
("id", Int32Wrap.SerdeInfo, typeof(Location).GetProperty("Id")!),
("address1", StringWrap.SerdeInfo, typeof(Location).GetProperty("Address1")!),
("address2", StringWrap.SerdeInfo, typeof(Location).GetProperty("Address2")!),
("city", StringWrap.SerdeInfo, typeof(Location).GetProperty("City")!),
Expand All @@ -86,48 +86,54 @@ public partial record LocationWrap : IDeserialize<Location>
string _l_country = default !;
ushort _r_assignedValid = 0b0;

var typeDeserialize = deserializer.DeserializeType(SerdeInfo);
var _l_serdeInfo = SerdeInfo;
var typeDeserialize = deserializer.ReadType(_l_serdeInfo);
int index;
while ((index = typeDeserialize.TryReadIndex(SerdeInfo, out _)) != IDeserializeType.EndOfType)
while ((index = typeDeserialize.TryReadIndex(_l_serdeInfo, out _)) != IDeserializeType.EndOfType)
{
switch (index)
{
case 0:
_l_id = typeDeserialize.ReadValue<int, Int32Wrap>(index);
_l_id = typeDeserialize.ReadI32(index);
_r_assignedValid |= ((ushort)1) << 0;
break;
case 1:
_l_address1 = typeDeserialize.ReadValue<string, StringWrap>(index);
_l_address1 = typeDeserialize.ReadString(index);
_r_assignedValid |= ((ushort)1) << 1;
break;
case 2:
_l_address2 = typeDeserialize.ReadValue<string, StringWrap>(index);
_l_address2 = typeDeserialize.ReadString(index);
_r_assignedValid |= ((ushort)1) << 2;
break;
case 3:
_l_city = typeDeserialize.ReadValue<string, StringWrap>(index);
_l_city = typeDeserialize.ReadString(index);
_r_assignedValid |= ((ushort)1) << 3;
break;
case 4:
_l_state = typeDeserialize.ReadValue<string, StringWrap>(index);
_l_state = typeDeserialize.ReadString(index);
_r_assignedValid |= ((ushort)1) << 4;
break;
case 5:
_l_postalcode = typeDeserialize.ReadValue<string, StringWrap>(index);
_l_postalcode = typeDeserialize.ReadString(index);
_r_assignedValid |= ((ushort)1) << 5;
break;
case 6:
_l_name = typeDeserialize.ReadValue<string, StringWrap>(index);
_l_name = typeDeserialize.ReadString(index);
_r_assignedValid |= ((ushort)1) << 6;
break;
case 7:
_l_phonenumber = typeDeserialize.ReadValue<string, StringWrap>(index);
_l_phonenumber = typeDeserialize.ReadString(index);
_r_assignedValid |= ((ushort)1) << 7;
break;
case 8:
_l_country = typeDeserialize.ReadValue<string, StringWrap>(index);
_l_country = typeDeserialize.ReadString(index);
_r_assignedValid |= ((ushort)1) << 8;
break;
case Serde.IDeserializeType.IndexNotFound:
typeDeserialize.SkipValue();
break;
default:
throw new InvalidOperationException("Unexpected index: " + index);
}
}

Expand Down
55 changes: 42 additions & 13 deletions src/generator/Generator.Deserialize.cs
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ internal static (MemberDeclarationSyntax[], BaseListSyntax) GenerateDeserializeI
/// static T IDeserialize&lt;T&gt;Deserialize(IDeserializer deserializer)
/// {
/// var serdeInfo = SerdeInfoProvider.GetInfo{T}();
/// var de = deserializer.DeserializeType(serdeInfo);
/// var de = deserializer.ReadType(serdeInfo);
/// int index;
/// if ((index = de.TryReadIndex(serdeInfo, out var errorName)) == IDeserializeType.IndexNotFound)
/// {
Expand Down Expand Up @@ -88,7 +88,7 @@ private static MethodDeclarationSyntax GenerateEnumDeserializeMethod(
static {{typeFqn}} IDeserialize<{{typeFqn}}>.Deserialize(IDeserializer deserializer)
{
var serdeInfo = global::Serde.SerdeInfoProvider.GetInfo<{{typeFqn}}Wrap>();
var de = deserializer.DeserializeType(serdeInfo);
var de = deserializer.ReadType(serdeInfo);
int index;
if ((index = de.TryReadIndex(serdeInfo, out var errorName)) == IDeserializeType.IndexNotFound)
{
Expand Down Expand Up @@ -148,6 +148,11 @@ private static MethodDeclarationSyntax GenerateCustomDeserializeMethod(

const string typeInfoLocalName = "_l_serdeInfo";
const string indexLocalName = "_l_index_";
const string IndexErrorName = "_l_errorName";

var errorNameOrDiscard = SymbolUtilities.GetTypeOptions(type).DenyUnknownMembers
? $"var {IndexErrorName}"
: "_";

var methodText = $$"""
static {{typeFqn}} Serde.IDeserialize<{{typeFqn}}>.Deserialize(IDeserializer deserializer)
Expand All @@ -156,9 +161,9 @@ private static MethodDeclarationSyntax GenerateCustomDeserializeMethod(
{{assignedVarType}} {{AssignedVarName}} = 0;

var {{typeInfoLocalName}} = global::Serde.SerdeInfoProvider.GetInfo<{{typeDeclContext.Name}}>();
var typeDeserialize = deserializer.DeserializeType({{typeInfoLocalName}});
var typeDeserialize = deserializer.ReadType({{typeInfoLocalName}});
int {{indexLocalName}};
while (({{indexLocalName}} = typeDeserialize.TryReadIndex({{typeInfoLocalName}}, out var _l_errorName)) != IDeserializeType.EndOfType)
while (({{indexLocalName}} = typeDeserialize.TryReadIndex({{typeInfoLocalName}}, out {{errorNameOrDiscard}})) != IDeserializeType.EndOfType)
{
switch ({{indexLocalName}})
{
Expand All @@ -177,15 +182,15 @@ private static MethodDeclarationSyntax GenerateCustomDeserializeMethod(
var localsBuilder = new StringBuilder();
long assignedMaskValue = 0;
var skippedIndices = new List<int>();
for (int i = 0; i < members.Count; i++)
for (int fieldIndex = 0; fieldIndex < members.Count; fieldIndex++)
{
if (members[i].SkipDeserialize)
if (members[fieldIndex].SkipDeserialize)
{
skippedIndices.Add(i);
skippedIndices.Add(fieldIndex);
continue;
}

var m = members[i];
var m = members[fieldIndex];
string wrapperName;
var memberType = m.Type.WithNullableAnnotation(m.NullableAnnotation).ToDisplayString();
if (Wrappers.TryGetExplicitWrapper(m, context, SerdeUsage.Deserialize, inProgress) is { } explicitWrap)
Expand All @@ -196,7 +201,7 @@ private static MethodDeclarationSyntax GenerateCustomDeserializeMethod(
{
wrapperName = memberType;
}
else if (Wrappers.TryGetImplicitWrapper(m.Type, context, SerdeUsage.Deserialize, inProgress) is { Wrapper: {} wrap })
else if (Wrappers.TryGetImplicitWrapper(m.Type, context, SerdeUsage.Deserialize, inProgress) is { Wrapper: { } wrap })
{
wrapperName = wrap.ToString();
}
Expand All @@ -213,18 +218,19 @@ private static MethodDeclarationSyntax GenerateCustomDeserializeMethod(
}
var localName = GetLocalName(m);
localsBuilder.AppendLine($"{memberType} {localName} = default!;");
var readValueCall = GetReadValueCall(memberType, wrapperName);
casesBuilder.AppendLine($"""
case {i}:
{localName} = typeDeserialize.ReadValue<{memberType}, {wrapperName}>({indexLocalName});
{AssignedVarName} |= (({assignedVarType})1) << {i};
case {fieldIndex}:
{localName} = typeDeserialize.{readValueCall}({indexLocalName});
{AssignedVarName} |= (({assignedVarType})1) << {fieldIndex};
break;
""");

// Require that the member is assigned if m.ThrowIfMissing is set, or if it is not nullable
// and ThrowIfMissing is unset
if (m.ThrowIfMissing == true || (!m.IsNullable && m.ThrowIfMissing == null))
{
assignedMaskValue |= 1L << i;
assignedMaskValue |= 1L << fieldIndex;
}
}
var unknownMemberBehavior = SymbolUtilities.GetTypeOptions(type).DenyUnknownMembers
Expand Down Expand Up @@ -252,6 +258,29 @@ private static MethodDeclarationSyntax GenerateCustomDeserializeMethod(
return (casesBuilder.ToString(),
localsBuilder.ToString(),
"0b" + Convert.ToString(assignedMaskValue, 2));

static string GetReadValueCall(
string memberType,
string wrapperName)
{
return memberType switch {
"bool" => "ReadBool",
"char" => "ReadChar",
"byte" => "ReadByte",
"ushort" => "ReadU16",
"uint" => "ReadU32",
"ulong" => "ReadU64",
"sbyte" => "ReadSByte",
"short" => "ReadI16",
"int" => "ReadI32",
"long" => "ReadI64",
"float" => "ReadFloat",
"double" => "ReadDouble",
"decimal" => "ReadDecimal",
"string" => "ReadString",
_ => $"ReadValue<{memberType}, {wrapperName}>"
};
}
}
}

Expand Down
52 changes: 33 additions & 19 deletions src/serde/IDeserialize.cs
Original file line number Diff line number Diff line change
Expand Up @@ -109,28 +109,42 @@ public interface IDeserializeType
V ReadValue<V, D>(int index) where D : IDeserialize<V>;

void SkipValue();

bool ReadBool(int index);
char ReadChar(int index);
byte ReadByte(int index);
ushort ReadU16(int index);
uint ReadU32(int index);
ulong ReadU64(int index);
sbyte ReadSByte(int index);
short ReadI16(int index);
int ReadI32(int index);
long ReadI64(int index);
float ReadFloat(int index);
double ReadDouble(int index);
decimal ReadDecimal(int index);
string ReadString(int index);
}

public interface IDeserializer : IDisposable
{
T DeserializeAny<T>(IDeserializeVisitor<T> v);
T DeserializeBool<T>(IDeserializeVisitor<T> v);
T DeserializeChar<T>(IDeserializeVisitor<T> v);
T DeserializeByte<T>(IDeserializeVisitor<T> v);
T DeserializeU16<T>(IDeserializeVisitor<T> v);
T DeserializeU32<T>(IDeserializeVisitor<T> v);
T DeserializeU64<T>(IDeserializeVisitor<T> v);
T DeserializeSByte<T>(IDeserializeVisitor<T> v);
T DeserializeI16<T>(IDeserializeVisitor<T> v);
T DeserializeI32<T>(IDeserializeVisitor<T> v);
T DeserializeI64<T>(IDeserializeVisitor<T> v);
T DeserializeFloat<T>(IDeserializeVisitor<T> v);
T DeserializeDouble<T>(IDeserializeVisitor<T> v);
T DeserializeDecimal<T>(IDeserializeVisitor<T> v);
T DeserializeString<T>(IDeserializeVisitor<T> v);
T DeserializeIdentifier<T>(IDeserializeVisitor<T> v);
T DeserializeNullableRef<T>(IDeserializeVisitor<T> v);
IDeserializeCollection DeserializeCollection(ISerdeInfo typeInfo);
IDeserializeType DeserializeType(ISerdeInfo typeInfo);
T ReadAny<T>(IDeserializeVisitor<T> v);
T ReadNullableRef<T>(IDeserializeVisitor<T> v);
bool ReadBool();
char ReadChar();
byte ReadByte();
ushort ReadU16();
uint ReadU32();
ulong ReadU64();
sbyte ReadSByte();
short ReadI16();
int ReadI32();
long ReadI64();
float ReadFloat();
double ReadDouble();
decimal ReadDecimal();
string ReadString();
IDeserializeCollection ReadCollection(ISerdeInfo typeInfo);
IDeserializeType ReadType(ISerdeInfo typeInfo);
}
}
4 changes: 2 additions & 2 deletions src/serde/Wrappers.Dictionary.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ public void Serialize(Dictionary<TKey, TValue> value, ISerializer serializer)
public static Dictionary<TKey, TValue> Deserialize(IDeserializer deserializer)
{
var typeInfo = DictSerdeInfo<TKey, TValue>.Instance;
var deCollection = deserializer.DeserializeCollection(typeInfo);
var deCollection = deserializer.ReadCollection(typeInfo);
Dictionary<TKey, TValue> dict;
if (deCollection.SizeOpt is int size)
{
Expand Down Expand Up @@ -96,7 +96,7 @@ public void Serialize(ImmutableDictionary<TKey, TValue> value, ISerializer seria
public static ImmutableDictionary<TKey, TValue> Deserialize(IDeserializer deserializer)
{
var typeInfo = DictSerdeInfo<TKey, TValue>.Instance;
var deCollection = deserializer.DeserializeCollection(typeInfo);
var deCollection = deserializer.ReadCollection(typeInfo);
var builder = ImmutableDictionary.CreateBuilder<TKey, TValue>();
while (deCollection.TryReadValue<TKey, TKeyWrap>(typeInfo, out var key))
{
Expand Down
6 changes: 3 additions & 3 deletions src/serde/Wrappers.List.cs
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ public void Serialize(T[] value, ISerializer serializer)
public static T[] Deserialize(IDeserializer deserializer)
{
var typeInfo = ArraySerdeTypeInfo<T>.TypeInfo;
var deCollection = deserializer.DeserializeCollection(typeInfo);
var deCollection = deserializer.ReadCollection(typeInfo);
if (deCollection.SizeOpt is int size)
{
var array = new T[size];
Expand Down Expand Up @@ -100,7 +100,7 @@ public static List<T> Deserialize(IDeserializer deserializer)
{
List<T> list;
var typeInfo = ListSerdeTypeInfo<T>.TypeInfo;
var deCollection = deserializer.DeserializeCollection(typeInfo);
var deCollection = deserializer.ReadCollection(typeInfo);
if (deCollection.SizeOpt is int size)
{
list = new List<T>(size);
Expand Down Expand Up @@ -146,7 +146,7 @@ public static ImmutableArray<T> Deserialize(IDeserializer deserializer)
{
ImmutableArray<T>.Builder builder;
var typeInfo = ImmutableArraySerdeTypeInfo<T>.TypeInfo;
var d = deserializer.DeserializeCollection(typeInfo);
var d = deserializer.ReadCollection(typeInfo);
if (d.SizeOpt is int size)
{
builder = ImmutableArray.CreateBuilder<T>(size);
Expand Down
Loading

0 comments on commit 8633dc0

Please sign in to comment.