Skip to content

Commit

Permalink
Allow using TypeInfo for deserializing collections (#168)
Browse files Browse the repository at this point in the history
Offers a simple replacement for both enumerables and dictionaries.
TypeInfo now needs to track the kind of Type it's describing --
enumerables, dictionaries, primitive types, and custom types should all
be handled differently.
  • Loading branch information
agocke committed Jun 17, 2024
1 parent 381a6fb commit 2057d6d
Show file tree
Hide file tree
Showing 101 changed files with 595 additions and 330 deletions.
2 changes: 1 addition & 1 deletion perf/bench/SampleTypes.cs
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ public partial record Location

public partial record LocationWrap : IDeserialize<Location>
{
private static readonly TypeInfo s_fieldMap = TypeInfo.Create([
private static readonly TypeInfo s_fieldMap = TypeInfo.Create(TypeInfo.TypeKind.CustomType, [
("id", typeof(Location).GetProperty("Id")!),
("address1", typeof(Location).GetProperty("Address1")!),
("address2", typeof(Location).GetProperty("Address2")!),
Expand Down
4 changes: 3 additions & 1 deletion src/generator/Generator.SerdeTypeInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,9 @@ internal static class SerdeTypeInfoGenerator
var newType = $$"""
internal static class {{typeName}}SerdeTypeInfo
{
internal static readonly Serde.TypeInfo TypeInfo = Serde.TypeInfo.Create(new (string, System.Reflection.MemberInfo)[] {
internal static readonly Serde.TypeInfo TypeInfo = Serde.TypeInfo.Create(
Serde.TypeInfo.TypeKind.CustomType,
new (string, System.Reflection.MemberInfo)[] {
{{string.Join("," + Environment.NewLine,
fieldsAndProps.Select(x => $@"(""{x.GetFormattedName()}"", typeof({typeString}).Get{(x.Symbol.Kind == SymbolKind.Field ? "Field" : "Property")}(""{x.Name}"")!)"))}}
});
Expand Down
129 changes: 9 additions & 120 deletions src/serde/IDeserialize.cs
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,14 @@ public interface IDeserializeVisitor<T>
T VisitNotNull(IDeserializer d) => throw new InvalidOperationException("Expected type " + ExpectedTypeName);
}

public interface IDeserializeCollection
{
int? SizeOpt { get; }

bool TryReadValue<T, D>(TypeInfo typeInfo, [MaybeNullWhen(false)] out T next)
where D : IDeserialize<T>;
}

public interface IDeserializeEnumerable
{
bool TryGetNext<T, D>([MaybeNullWhen(false)] out T next)
Expand Down Expand Up @@ -93,126 +101,6 @@ public interface IDeserializeType
V ReadValue<V, D>(int index) where D : IDeserialize<V>;
}

/// <summary>
/// TypeInfo holds a variety of indexed information about a type. The most important is a map
/// from field names to int indices. This is an optimization for deserializing types that avoids
/// allocating strings for field names.
///
/// It can also be used to get the custom attributes for a field.
/// </summary>
public sealed class TypeInfo
{
// The field names are sorted by the Utf8 representation of the field name.
private readonly ImmutableArray<(ReadOnlyMemory<byte> Utf8Name, int Index)> _nameToIndex;
private readonly ImmutableArray<PrivateFieldInfo> _indexToInfo;

private TypeInfo(
ImmutableArray<(ReadOnlyMemory<byte>, int)> nameToIndex,
ImmutableArray<PrivateFieldInfo> indexToInfo)
{
_nameToIndex = nameToIndex;
_indexToInfo = indexToInfo;
}

/// <summary>
/// Holds information for a field or property in the given type.
/// </summary>
private readonly record struct PrivateFieldInfo(IList<CustomAttributeData> CustomAttributesData);


private static readonly UTF8Encoding s_utf8 = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false);

/// <summary>
/// Create a new field mapping. The ordering of the fields is important -- it
/// corresponds to the index returned by <see cref="IDeserializeType.TryReadIndex" />.
/// </summary>
public static TypeInfo Create(
ReadOnlySpan<(string SerializeName, MemberInfo MemberInfo)> fields)
{
var nameToIndexBuilder = ImmutableArray.CreateBuilder<(ReadOnlyMemory<byte> Utf8Name, int Index)>(fields.Length);
var indexToInfoBuilder = ImmutableArray.CreateBuilder<PrivateFieldInfo>(fields.Length);
for (int index = 0; index < fields.Length; index++)
{
var (serializeName, memberInfo) = fields[index];
if (memberInfo is null)
{
throw new ArgumentNullException(serializeName);
}

nameToIndexBuilder.Add((s_utf8.GetBytes(serializeName), index));
var fieldInfo = new PrivateFieldInfo(memberInfo.GetCustomAttributesData());
indexToInfoBuilder.Add(fieldInfo);
}

nameToIndexBuilder.Sort((left, right) =>
left.Utf8Name.Span.SequenceCompareTo(right.Utf8Name.Span));

return new TypeInfo(nameToIndexBuilder.ToImmutable(), indexToInfoBuilder.ToImmutable());
}

/// <summary>
/// Returns an index corresponding to the location of the field in the original
/// ReadOnlySpan passed during creation of the <see cref="TypeInfo"/>. This can be
/// used as a fast lookup for a field based on its UTF-8 name.
/// </summary>
public int TryGetIndex(Utf8Span utf8FieldName)
{
int mapIndex = BinarySearch(_nameToIndex.AsSpan(), utf8FieldName);

return mapIndex < 0 ? IDeserializeType.IndexNotFound : _nameToIndex[mapIndex].Index;
}

public IList<CustomAttributeData> GetCustomAttributeData(int index)
{
return _indexToInfo[index].CustomAttributesData;
}


[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static int BinarySearch(ReadOnlySpan<(ReadOnlyMemory<byte>, int)> span, Utf8Span fieldName)
{
return BinarySearch(ref MemoryMarshal.GetReference(span), span.Length, fieldName);
}

// This is a copy of the BinarySearch method from System.MemoryExtensions.
// We can't use that version because ref structs can't yet be substituted for type arguments.
private static int BinarySearch(ref (ReadOnlyMemory<byte> Utf8Name, int) spanStart, int length, Utf8Span fieldName)
{
int lo = 0;
int hi = length - 1;
// If length == 0, hi == -1, and loop will not be entered
while (lo <= hi)
{
// PERF: `lo` or `hi` will never be negative inside the loop,
// so computing median using uints is safe since we know
// `length <= int.MaxValue`, and indices are >= 0
// and thus cannot overflow an uint.
// Saves one subtraction per loop compared to
// `int i = lo + ((hi - lo) >> 1);`
int i = (int)(((uint)hi + (uint)lo) >> 1);

int c = fieldName.SequenceCompareTo(Unsafe.Add(ref spanStart, i).Utf8Name.Span);
if (c == 0)
{
return i;
}
else if (c > 0)
{
lo = i + 1;
}
else
{
hi = i - 1;
}
}
// If none found, then a negative number that is the bitwise complement
// of the index of the next element that is larger than or, if there is
// no larger element, the bitwise complement of `length`, which
// is `lo` at this point.
return ~lo;
}
}

public interface IDeserializer
{
T DeserializeAny<T>(IDeserializeVisitor<T> v);
Expand All @@ -235,6 +123,7 @@ public interface IDeserializer
T DeserializeEnumerable<T>(IDeserializeVisitor<T> v);
T DeserializeDictionary<T>(IDeserializeVisitor<T> v);
T DeserializeNullableRef<T>(IDeserializeVisitor<T> v);
IDeserializeCollection DeserializeCollection(TypeInfo typeInfo) => throw new NotImplementedException();
IDeserializeType DeserializeType(TypeInfo typeInfo) => throw new NotImplementedException();
}
}
145 changes: 145 additions & 0 deletions src/serde/TypeInfo.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@

using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Text;

namespace Serde;

/// <summary>
/// TypeInfo holds a variety of indexed information about a type. The most important is a map
/// from field names to int indices. This is an optimization for deserializing types that avoids
/// allocating strings for field names.
///
/// It can also be used to get the custom attributes for a field.
/// </summary>
public sealed class TypeInfo
{
public enum TypeKind
{
Primitive,
CustomType,
Enumerable,
Dictionary,
}

public TypeKind Kind { get; }

/// <summary>
/// Returns an index corresponding to the location of the field in the original
/// ReadOnlySpan passed during creation of the <see cref="TypeInfo"/>. This can be
/// used as a fast lookup for a field based on its UTF-8 name.
/// </summary>
public int TryGetIndex(Utf8Span utf8FieldName)
{
int mapIndex = BinarySearch(_nameToIndex.AsSpan(), utf8FieldName);

return mapIndex < 0 ? IDeserializeType.IndexNotFound : _nameToIndex[mapIndex].Index;
}

public IList<CustomAttributeData> GetCustomAttributeData(int index)
{
return _indexToInfo[index].CustomAttributesData;
}

/// <summary>
/// Create a new field mapping. The ordering of the fields is important -- it
/// corresponds to the index returned by <see cref="IDeserializeType.TryReadIndex" />.
/// </summary>
public static TypeInfo Create(
TypeKind typeKind,
ReadOnlySpan<(string SerializeName, MemberInfo MemberInfo)> fields)
{
var nameToIndexBuilder = ImmutableArray.CreateBuilder<(ReadOnlyMemory<byte> Utf8Name, int Index)>(fields.Length);
var indexToInfoBuilder = ImmutableArray.CreateBuilder<PrivateFieldInfo>(fields.Length);
for (int index = 0; index < fields.Length; index++)
{
var (serializeName, memberInfo) = fields[index];
if (memberInfo is null)
{
throw new ArgumentNullException(serializeName);
}

nameToIndexBuilder.Add((s_utf8.GetBytes(serializeName), index));
var fieldInfo = new PrivateFieldInfo(memberInfo.GetCustomAttributesData());
indexToInfoBuilder.Add(fieldInfo);
}

nameToIndexBuilder.Sort((left, right) =>
left.Utf8Name.Span.SequenceCompareTo(right.Utf8Name.Span));

return new TypeInfo(typeKind, nameToIndexBuilder.ToImmutable(), indexToInfoBuilder.ToImmutable());
}

#region Private implementation details

// The field names are sorted by the Utf8 representation of the field name.
private readonly ImmutableArray<(ReadOnlyMemory<byte> Utf8Name, int Index)> _nameToIndex;
private readonly ImmutableArray<PrivateFieldInfo> _indexToInfo;

/// <summary>
/// Holds information for a field or property in the given type.
/// </summary>
private readonly record struct PrivateFieldInfo(IList<CustomAttributeData> CustomAttributesData);

private static readonly UTF8Encoding s_utf8 = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false);

private TypeInfo(
TypeKind typeKind,
ImmutableArray<(ReadOnlyMemory<byte>, int)> nameToIndex,
ImmutableArray<PrivateFieldInfo> indexToInfo)
{
Kind = typeKind;
_nameToIndex = nameToIndex;
_indexToInfo = indexToInfo;
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static int BinarySearch(ReadOnlySpan<(ReadOnlyMemory<byte>, int)> span, Utf8Span fieldName)
{
return BinarySearch(ref MemoryMarshal.GetReference(span), span.Length, fieldName);
}

// This is a copy of the BinarySearch method from System.MemoryExtensions.
// We can't use that version because ref structs can't yet be substituted for type arguments.
private static int BinarySearch(ref (ReadOnlyMemory<byte> Utf8Name, int) spanStart, int length, Utf8Span fieldName)
{
int lo = 0;
int hi = length - 1;
// If length == 0, hi == -1, and loop will not be entered
while (lo <= hi)
{
// PERF: `lo` or `hi` will never be negative inside the loop,
// so computing median using uints is safe since we know
// `length <= int.MaxValue`, and indices are >= 0
// and thus cannot overflow an uint.
// Saves one subtraction per loop compared to
// `int i = lo + ((hi - lo) >> 1);`
int i = (int)(((uint)hi + (uint)lo) >> 1);

int c = fieldName.SequenceCompareTo(Unsafe.Add(ref spanStart, i).Utf8Name.Span);
if (c == 0)
{
return i;
}
else if (c > 0)
{
lo = i + 1;
}
else
{
hi = i - 1;
}
}
// If none found, then a negative number that is the bitwise complement
// of the index of the next element that is larger than or, if there is
// no larger element, the bitwise complement of `length`, which
// is `lo` at this point.
return ~lo;
}

#endregion // Private implementation details
}
Loading

0 comments on commit 2057d6d

Please sign in to comment.