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

Allow using TypeInfo for deserializing collections #168

Merged
merged 3 commits into from
Jun 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading