Skip to content

Commit

Permalink
Merge pull request #59 from Cysharp/improve-custom-formatter
Browse files Browse the repository at this point in the history
Improve CustomFormatter performance
  • Loading branch information
neuecc authored Nov 20, 2022
2 parents 35c48df + 45f7170 commit 299f759
Show file tree
Hide file tree
Showing 19 changed files with 504 additions and 28 deletions.
23 changes: 21 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -583,7 +583,7 @@ public partial class Employee

CustomFormatter
---
If implements `MemoryPackCustomFormatterAttribute<T>`, you can configure to use custom formatter to MemoryPackObject's member.
If implements `MemoryPackCustomFormatterAttribute<T>` or `MemoryPackCustomFormatterAttribute<TFormatter, T>`(more performant but complex), you can configure to use custom formatter to MemoryPackObject's member.

```csharp
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = false, Inherited = false)]
Expand All @@ -593,17 +593,23 @@ public abstract class MemoryPackCustomFormatterAttribute<T> : Attribute
}
```

In built-in attribtues, `Utf8StringFormatterAttribute`, `Utf16StringFormatterAttribute`, `OrdinalIgnoreCaseStringDictionaryFormatter<TValue>` exsits.
In built-in attribtues, `Utf8StringFormatterAttribute`, `Utf16StringFormatterAttribute`, `InternStringFormatterAttribute`, `OrdinalIgnoreCaseStringDictionaryFormatterAttribtue<TValue>`, `BitPackFormatterAttribtue` exsits.

```csharp
[MemoryPackable]
public partial class Sample
{
// serialize this member as UTF16 String, it is performant than UTF8 but in ASCII, size is larger(but non ASCII, sometimes smaller).
[Utf16StringFormatter]
public string? Text { get; set; }

// In deserialize, Dictionary is initialized with StringComparer.OrdinalIgnoreCase.
[OrdinalIgnoreCaseStringDictionaryFormatter<int>]
public Dictionary<string, int>? Ids { get; set; }

// In deserialize time, all string is interned(see: String.Intern). If similar values come repeatedly, it saves memory.
[InternStringFormatter]
public string? Flag { get; set; }
}
```

Expand All @@ -621,6 +627,19 @@ public sealed class OrdinalIgnoreCaseStringDictionaryFormatter<TValue> : MemoryP
}
```

`BitPackFormatter` is for `bool[]`, same serialzied result as `BitArray`. In other words, bool is normally 1byte, but since it is treated as 1bit, eight bools are stored in one byte. Therefore, the size after serialization is 1/8.

```csharp
[MemoryPackable]
public partial class Sample
{
public int Id { get; set; }

[BitPackFormatter]
public bool[]? Data { get; set; }
}
```

Performance
---
TODO for describe details, stay tuned.
Expand Down
7 changes: 7 additions & 0 deletions src/MemoryPack.Core/Attributes.cs
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,13 @@ public abstract class MemoryPackCustomFormatterAttribute<T> : Attribute
public abstract IMemoryPackFormatter<T> GetFormatter();
}

[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = false, Inherited = false)]
public abstract class MemoryPackCustomFormatterAttribute<TFormatter, T> : Attribute
where TFormatter : IMemoryPackFormatter<T>
{
public abstract TFormatter GetFormatter();
}

#endif

// similar naming as System.Text.Json attribtues
Expand Down
110 changes: 110 additions & 0 deletions src/MemoryPack.Core/Compression/BitPackFormatter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
using MemoryPack.Internal;
using System.Runtime.CompilerServices;

namespace MemoryPack.Compression;

[Preserve]
public sealed class BitPackFormatter : MemoryPackFormatter<bool[]>
{
public static readonly BitPackFormatter Default = new BitPackFormatter();

[Preserve]
public override void Serialize<TBufferWriter>(ref MemoryPackWriter<TBufferWriter> writer, scoped ref bool[]? value)
{
if (value == null)
{
writer.WriteNullCollectionHeader();
return;
}
writer.WriteCollectionHeader(value.Length);

var data = 0;
var bit = 0;
foreach (var item in value)
{
Set(ref data, bit, item);

bit += 1;

if (bit == 32)
{
writer.WriteUnmanaged(data);
data = 0;
bit = 0;
}
}

if (bit != 0)
{
writer.WriteUnmanaged(data);
}
}

[Preserve]
public override void Deserialize(ref MemoryPackReader reader, scoped ref bool[]? value)
{
if (!reader.DangerousTryReadCollectionHeader(out var length))
{
value = null;
return;
}

if (length == 0)
{
value = Array.Empty<bool>();
return;
}

var readCount = ((length - 1) / 32) + 1;
var requireSize = readCount * 4;
if (reader.Remaining < requireSize)
{
MemoryPackSerializationException.ThrowInsufficientBufferUnless(length);
}

if (value == null || value.Length != length)
{
value = new bool[length];
}

var bit = 0;
var data = 0;
for (int i = 0; i < value.Length; i++)
{
if (bit == 0)
{
reader.ReadUnmanaged(out data);
}

value[i] = Get(data, bit);

bit += 1;

if (bit == 32)
{
data = 0;
bit = 0;
}
}
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool Get(int data, int index)
{
return (data & (1 << index)) != 0;
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Set(ref int data, int index, bool value)
{
int bitMask = 1 << index;
if (value)
{
data |= bitMask;
}
else
{
data &= ~bitMask;
}
}
}
31 changes: 24 additions & 7 deletions src/MemoryPack.Core/CustomFormatterAttributes.cs
Original file line number Diff line number Diff line change
@@ -1,33 +1,50 @@
using MemoryPack.Formatters;
using MemoryPack.Compression;
using MemoryPack.Formatters;

namespace MemoryPack;

#if !UNITY_2021_2_OR_NEWER

public sealed class Utf8StringFormatterAttribute : MemoryPackCustomFormatterAttribute<string>
public sealed class Utf8StringFormatterAttribute : MemoryPackCustomFormatterAttribute<Utf8StringFormatter, string>
{
public override IMemoryPackFormatter<string> GetFormatter()
public override Utf8StringFormatter GetFormatter()
{
return Utf8StringFormatter.Default;
}
}

public sealed class Utf16StringFormatterAttribute : MemoryPackCustomFormatterAttribute<string>
public sealed class Utf16StringFormatterAttribute : MemoryPackCustomFormatterAttribute<Utf16StringFormatter, string>
{
public override IMemoryPackFormatter<string> GetFormatter()
public override Utf16StringFormatter GetFormatter()
{
return Utf16StringFormatter.Default;
}
}

public sealed class OrdinalIgnoreCaseStringDictionaryFormatter<TValue> : MemoryPackCustomFormatterAttribute<Dictionary<string, TValue?>>
public sealed class OrdinalIgnoreCaseStringDictionaryFormatter<TValue> : MemoryPackCustomFormatterAttribute<DictionaryFormatter<string, TValue?>, Dictionary<string, TValue?>>
{
static readonly DictionaryFormatter<string, TValue?> formatter = new DictionaryFormatter<string, TValue?>(StringComparer.OrdinalIgnoreCase);

public override IMemoryPackFormatter<Dictionary<string, TValue?>> GetFormatter()
public override DictionaryFormatter<string, TValue?> GetFormatter()
{
return formatter;
}
}

public sealed class InternStringFormatterAttribute : MemoryPackCustomFormatterAttribute<InternStringFormatter, string>
{
public override InternStringFormatter GetFormatter()
{
return InternStringFormatter.Default;
}
}

public sealed class BitPackFormatterAttribute : MemoryPackCustomFormatterAttribute<BitPackFormatter, bool[]>
{
public override BitPackFormatter GetFormatter()
{
return BitPackFormatter.Default;
}
}

#endif
32 changes: 32 additions & 0 deletions src/MemoryPack.Core/Formatters/StringFormatter.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using MemoryPack.Internal;
using System.Runtime.CompilerServices;

namespace MemoryPack.Formatters;

Expand Down Expand Up @@ -26,12 +27,14 @@ public sealed class Utf8StringFormatter : MemoryPackFormatter<string>
public static readonly Utf8StringFormatter Default = new Utf8StringFormatter();

[Preserve]
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public override void Serialize<TBufferWriter>(ref MemoryPackWriter<TBufferWriter> writer, scoped ref string? value)
{
writer.WriteUtf8(value);
}

[Preserve]
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public override void Deserialize(ref MemoryPackReader reader, scoped ref string? value)
{
value = reader.ReadString();
Expand All @@ -44,14 +47,43 @@ public sealed class Utf16StringFormatter : MemoryPackFormatter<string>
public static readonly Utf16StringFormatter Default = new Utf16StringFormatter();

[Preserve]
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public override void Serialize<TBufferWriter>(ref MemoryPackWriter<TBufferWriter> writer, scoped ref string? value)
{
writer.WriteUtf16(value);
}

[Preserve]
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public override void Deserialize(ref MemoryPackReader reader, scoped ref string? value)
{
value = reader.ReadString();
}
}

[Preserve]
public sealed class InternStringFormatter : MemoryPackFormatter<string>
{
public static readonly InternStringFormatter Default = new InternStringFormatter();

[Preserve]
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public override void Serialize<TBufferWriter>(ref MemoryPackWriter<TBufferWriter> writer, scoped ref string? value)
{
writer.WriteString(value);
}

[Preserve]
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public override void Deserialize(ref MemoryPackReader reader, scoped ref string? value)
{
var str = reader.ReadString();
if (str == null)
{
value = null;
return;
}

value = string.Intern(str);
}
}
3 changes: 3 additions & 0 deletions src/MemoryPack.Core/MemoryPack.Core.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,9 @@
<ItemGroup>
<TargetFiles1 Include="$(MSBuildProjectDirectory)\**\*.cs" Exclude="**\bin\**\*.*;**\obj\**\*.*;**\CompressedArray.cs;**\CollectionsMarshalEx.cs;**\ImmutableCollectionFormatters.cs" />
</ItemGroup>
<ItemGroup>
<TargetFiles1 Remove="BitPackFormatter.cs" />
</ItemGroup>

<Target Name="PostBuild" AfterTargets="PostBuildEvent" Condition="$(TargetFramework) == 'net7.0'">
<Copy SourceFiles="@(TargetFiles1)" DestinationFiles="$(DestinationRoot)\%(RecursiveDir)%(Filename)%(Extension)" SkipUnchangedFiles="true" />
Expand Down
5 changes: 3 additions & 2 deletions src/MemoryPack.Generator/MemoryPackGenerator.Emitter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -562,7 +562,8 @@ string EmitCustomFormatters()
foreach (var item in Members.Where(x => x.Kind == MemberKind.CustomFormatter))
{
var fieldOrProp = item.IsField ? "Field" : "Property";
sb.AppendLine($" static readonly IMemoryPackFormatter<{item.MemberType.FullyQualifiedToString()}> __{item.Name}Formatter = System.Reflection.CustomAttributeExtensions.GetCustomAttribute<{item.CustomFormatter!.FullyQualifiedToString()}>(typeof({this.Symbol.FullyQualifiedToString()}).Get{fieldOrProp}(\"{item.Name}\", System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance)).GetFormatter();");

sb.AppendLine($" static readonly {item.CustomFormatterName} __{item.Name}Formatter = System.Reflection.CustomAttributeExtensions.GetCustomAttribute<{item.CustomFormatter!.FullyQualifiedToString()}>(typeof({this.Symbol.FullyQualifiedToString()}).Get{fieldOrProp}(\"{item.Name}\", System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance)).GetFormatter();");
}
return sb.ToString();
}
Expand Down Expand Up @@ -1203,7 +1204,7 @@ public string EmitReadToDeserialize(int i, bool requireDeltaCheck)
case MemberKind.CustomFormatter:
{
var mt = MemberType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
return $"{pre}__{Name} = reader.ReadValueWithFormatter<IMemoryPackFormatter<{mt}>, {mt}>(__{Name}Formatter);";
return $"{pre}__{Name} = reader.ReadValueWithFormatter<{CustomFormatterName}, {mt}>(__{Name}Formatter);";
}
default:
return $"{pre}__{Name} = reader.ReadValue<{MemberType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)}>();";
Expand Down
24 changes: 22 additions & 2 deletions src/MemoryPack.Generator/MemoryPackGenerator.Parser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -461,7 +461,8 @@ partial class MemberMeta
public ISymbol Symbol { get; }
public string Name { get; }
public ITypeSymbol MemberType { get; }
public INamedTypeSymbol? CustomFormatter { get; set; }
public INamedTypeSymbol? CustomFormatter { get; }
public string? CustomFormatterName { get; }
public bool IsField { get; }
public bool IsProperty { get; }
public bool IsSettable { get; }
Expand Down Expand Up @@ -538,11 +539,30 @@ public MemberMeta(ISymbol symbol, IMethodSymbol? constructor, ReferenceSymbols r

if (references.MemoryPackCustomFormatterAttribute != null)
{
var genericFormatter = false;
var customFormatterAttr = symbol.GetImplAttribute(references.MemoryPackCustomFormatterAttribute);
if (customFormatterAttr == null && references.MemoryPackCustomFormatter2Attribute != null)
{
customFormatterAttr = symbol.GetImplAttribute(references.MemoryPackCustomFormatter2Attribute);
genericFormatter = true;
}

if (customFormatterAttr != null)
{
CustomFormatter = customFormatterAttr.AttributeClass;
CustomFormatter = customFormatterAttr.AttributeClass!;
Kind = MemberKind.CustomFormatter;

string formatterName;
if (genericFormatter)
{
formatterName = CustomFormatter.GetAllBaseTypes().First(x => x.EqualsUnconstructedGenericType(references.MemoryPackCustomFormatter2Attribute!))
.TypeArguments[0].FullyQualifiedToString();
}
else
{
formatterName = $"IMemoryPackFormatter<{MemberType.FullyQualifiedToString()}>";
}
CustomFormatterName = formatterName;
return;
}
}
Expand Down
2 changes: 1 addition & 1 deletion src/MemoryPack.Generator/Properties/launchSettings.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"profiles": {
"MemoryPack.Generator": {
"commandName": "DebugRoslynComponent",
"targetProject": "..\\..\\sandbox\\ClassLibrary\\ClassLibrary.csproj"
"targetProject": "..\\..\\tests\\MemoryPack.Tests\\MemoryPack.Tests.csproj"
}
}
}
2 changes: 2 additions & 0 deletions src/MemoryPack.Generator/ReferenceSymbols.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ public class ReferenceSymbols
public INamedTypeSymbol MemoryPackAllowSerializeAttribute { get; }
public INamedTypeSymbol MemoryPackOrderAttribute { get; }
public INamedTypeSymbol? MemoryPackCustomFormatterAttribute { get; } // Unity is null.
public INamedTypeSymbol? MemoryPackCustomFormatter2Attribute { get; } // Unity is null.
public INamedTypeSymbol MemoryPackIgnoreAttribute { get; }
public INamedTypeSymbol MemoryPackIncludeAttribute { get; }
public INamedTypeSymbol MemoryPackOnSerializingAttribute { get; }
Expand All @@ -35,6 +36,7 @@ public ReferenceSymbols(Compilation compilation)
MemoryPackAllowSerializeAttribute = GetTypeByMetadataName("MemoryPack.MemoryPackAllowSerializeAttribute");
MemoryPackOrderAttribute = GetTypeByMetadataName("MemoryPack.MemoryPackOrderAttribute");
MemoryPackCustomFormatterAttribute = compilation.GetTypeByMetadataName("MemoryPack.MemoryPackCustomFormatterAttribute`1")?.ConstructUnboundGenericType();
MemoryPackCustomFormatter2Attribute = compilation.GetTypeByMetadataName("MemoryPack.MemoryPackCustomFormatterAttribute`2")?.ConstructUnboundGenericType();
MemoryPackIgnoreAttribute = GetTypeByMetadataName("MemoryPack.MemoryPackIgnoreAttribute");
MemoryPackIncludeAttribute = GetTypeByMetadataName("MemoryPack.MemoryPackIncludeAttribute");
MemoryPackOnSerializingAttribute = GetTypeByMetadataName("MemoryPack.MemoryPackOnSerializingAttribute");
Expand Down
Loading

0 comments on commit 299f759

Please sign in to comment.