Skip to content

Commit 9ebf18d

Browse files
committed
Improve CustomFormatter perf, Add InternStringFormatterAttribute
1 parent 35c48df commit 9ebf18d

File tree

12 files changed

+169
-19
lines changed

12 files changed

+169
-19
lines changed

src/MemoryPack.Core/Attributes.cs

+7
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,13 @@ public abstract class MemoryPackCustomFormatterAttribute<T> : Attribute
7777
public abstract IMemoryPackFormatter<T> GetFormatter();
7878
}
7979

80+
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = false, Inherited = false)]
81+
public abstract class MemoryPackCustomFormatterAttribute<TFormatter, T> : Attribute
82+
where TFormatter : IMemoryPackFormatter<T>
83+
{
84+
public abstract TFormatter GetFormatter();
85+
}
86+
8087
#endif
8188

8289
// similar naming as System.Text.Json attribtues

src/MemoryPack.Core/CustomFormatterAttributes.cs

+14-6
Original file line numberDiff line numberDiff line change
@@ -4,30 +4,38 @@ namespace MemoryPack;
44

55
#if !UNITY_2021_2_OR_NEWER
66

7-
public sealed class Utf8StringFormatterAttribute : MemoryPackCustomFormatterAttribute<string>
7+
public sealed class Utf8StringFormatterAttribute : MemoryPackCustomFormatterAttribute<Utf8StringFormatter, string>
88
{
9-
public override IMemoryPackFormatter<string> GetFormatter()
9+
public override Utf8StringFormatter GetFormatter()
1010
{
1111
return Utf8StringFormatter.Default;
1212
}
1313
}
1414

15-
public sealed class Utf16StringFormatterAttribute : MemoryPackCustomFormatterAttribute<string>
15+
public sealed class Utf16StringFormatterAttribute : MemoryPackCustomFormatterAttribute<Utf16StringFormatter, string>
1616
{
17-
public override IMemoryPackFormatter<string> GetFormatter()
17+
public override Utf16StringFormatter GetFormatter()
1818
{
1919
return Utf16StringFormatter.Default;
2020
}
2121
}
2222

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

27-
public override IMemoryPackFormatter<Dictionary<string, TValue?>> GetFormatter()
27+
public override DictionaryFormatter<string, TValue?> GetFormatter()
2828
{
2929
return formatter;
3030
}
3131
}
3232

33+
public sealed class InternStringFormatterAttribute : MemoryPackCustomFormatterAttribute<InternStringFormatter, string>
34+
{
35+
public override InternStringFormatter GetFormatter()
36+
{
37+
return InternStringFormatter.Default;
38+
}
39+
}
40+
3341
#endif

src/MemoryPack.Core/Formatters/StringFormatter.cs

+32
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using MemoryPack.Internal;
2+
using System.Runtime.CompilerServices;
23

34
namespace MemoryPack.Formatters;
45

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

2829
[Preserve]
30+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
2931
public override void Serialize<TBufferWriter>(ref MemoryPackWriter<TBufferWriter> writer, scoped ref string? value)
3032
{
3133
writer.WriteUtf8(value);
3234
}
3335

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

4649
[Preserve]
50+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
4751
public override void Serialize<TBufferWriter>(ref MemoryPackWriter<TBufferWriter> writer, scoped ref string? value)
4852
{
4953
writer.WriteUtf16(value);
5054
}
5155

5256
[Preserve]
57+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
5358
public override void Deserialize(ref MemoryPackReader reader, scoped ref string? value)
5459
{
5560
value = reader.ReadString();
5661
}
5762
}
63+
64+
[Preserve]
65+
public sealed class InternStringFormatter : MemoryPackFormatter<string>
66+
{
67+
public static readonly InternStringFormatter Default = new InternStringFormatter();
68+
69+
[Preserve]
70+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
71+
public override void Serialize<TBufferWriter>(ref MemoryPackWriter<TBufferWriter> writer, scoped ref string? value)
72+
{
73+
writer.WriteString(value);
74+
}
75+
76+
[Preserve]
77+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
78+
public override void Deserialize(ref MemoryPackReader reader, scoped ref string? value)
79+
{
80+
var str = reader.ReadString();
81+
if (str == null)
82+
{
83+
value = null;
84+
return;
85+
}
86+
87+
value = string.Intern(str);
88+
}
89+
}

src/MemoryPack.Generator/MemoryPackGenerator.Emitter.cs

+3-2
Original file line numberDiff line numberDiff line change
@@ -562,7 +562,8 @@ string EmitCustomFormatters()
562562
foreach (var item in Members.Where(x => x.Kind == MemberKind.CustomFormatter))
563563
{
564564
var fieldOrProp = item.IsField ? "Field" : "Property";
565-
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();");
565+
566+
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();");
566567
}
567568
return sb.ToString();
568569
}
@@ -1203,7 +1204,7 @@ public string EmitReadToDeserialize(int i, bool requireDeltaCheck)
12031204
case MemberKind.CustomFormatter:
12041205
{
12051206
var mt = MemberType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
1206-
return $"{pre}__{Name} = reader.ReadValueWithFormatter<IMemoryPackFormatter<{mt}>, {mt}>(__{Name}Formatter);";
1207+
return $"{pre}__{Name} = reader.ReadValueWithFormatter<{CustomFormatterName}, {mt}>(__{Name}Formatter);";
12071208
}
12081209
default:
12091210
return $"{pre}__{Name} = reader.ReadValue<{MemberType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)}>();";

src/MemoryPack.Generator/MemoryPackGenerator.Parser.cs

+22-2
Original file line numberDiff line numberDiff line change
@@ -461,7 +461,8 @@ partial class MemberMeta
461461
public ISymbol Symbol { get; }
462462
public string Name { get; }
463463
public ITypeSymbol MemberType { get; }
464-
public INamedTypeSymbol? CustomFormatter { get; set; }
464+
public INamedTypeSymbol? CustomFormatter { get; }
465+
public string? CustomFormatterName { get; }
465466
public bool IsField { get; }
466467
public bool IsProperty { get; }
467468
public bool IsSettable { get; }
@@ -538,11 +539,30 @@ public MemberMeta(ISymbol symbol, IMethodSymbol? constructor, ReferenceSymbols r
538539

539540
if (references.MemoryPackCustomFormatterAttribute != null)
540541
{
542+
var genericFormatter = false;
541543
var customFormatterAttr = symbol.GetImplAttribute(references.MemoryPackCustomFormatterAttribute);
544+
if (customFormatterAttr == null && references.MemoryPackCustomFormatter2Attribute != null)
545+
{
546+
customFormatterAttr = symbol.GetImplAttribute(references.MemoryPackCustomFormatter2Attribute);
547+
genericFormatter = true;
548+
}
549+
542550
if (customFormatterAttr != null)
543551
{
544-
CustomFormatter = customFormatterAttr.AttributeClass;
552+
CustomFormatter = customFormatterAttr.AttributeClass!;
545553
Kind = MemberKind.CustomFormatter;
554+
555+
string formatterName;
556+
if (genericFormatter)
557+
{
558+
formatterName = CustomFormatter.GetAllBaseTypes().First(x => x.EqualsUnconstructedGenericType(references.MemoryPackCustomFormatter2Attribute!))
559+
.TypeArguments[0].FullyQualifiedToString();
560+
}
561+
else
562+
{
563+
formatterName = $"IMemoryPackFormatter<{MemberType.FullyQualifiedToString()}>";
564+
}
565+
CustomFormatterName = formatterName;
546566
return;
547567
}
548568
}

src/MemoryPack.Generator/Properties/launchSettings.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"profiles": {
33
"MemoryPack.Generator": {
44
"commandName": "DebugRoslynComponent",
5-
"targetProject": "..\\..\\sandbox\\ClassLibrary\\ClassLibrary.csproj"
5+
"targetProject": "..\\..\\tests\\MemoryPack.Tests\\MemoryPack.Tests.csproj"
66
}
77
}
88
}

src/MemoryPack.Generator/ReferenceSymbols.cs

+2
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ public class ReferenceSymbols
1313
public INamedTypeSymbol MemoryPackAllowSerializeAttribute { get; }
1414
public INamedTypeSymbol MemoryPackOrderAttribute { get; }
1515
public INamedTypeSymbol? MemoryPackCustomFormatterAttribute { get; } // Unity is null.
16+
public INamedTypeSymbol? MemoryPackCustomFormatter2Attribute { get; } // Unity is null.
1617
public INamedTypeSymbol MemoryPackIgnoreAttribute { get; }
1718
public INamedTypeSymbol MemoryPackIncludeAttribute { get; }
1819
public INamedTypeSymbol MemoryPackOnSerializingAttribute { get; }
@@ -35,6 +36,7 @@ public ReferenceSymbols(Compilation compilation)
3536
MemoryPackAllowSerializeAttribute = GetTypeByMetadataName("MemoryPack.MemoryPackAllowSerializeAttribute");
3637
MemoryPackOrderAttribute = GetTypeByMetadataName("MemoryPack.MemoryPackOrderAttribute");
3738
MemoryPackCustomFormatterAttribute = compilation.GetTypeByMetadataName("MemoryPack.MemoryPackCustomFormatterAttribute`1")?.ConstructUnboundGenericType();
39+
MemoryPackCustomFormatter2Attribute = compilation.GetTypeByMetadataName("MemoryPack.MemoryPackCustomFormatterAttribute`2")?.ConstructUnboundGenericType();
3840
MemoryPackIgnoreAttribute = GetTypeByMetadataName("MemoryPack.MemoryPackIgnoreAttribute");
3941
MemoryPackIncludeAttribute = GetTypeByMetadataName("MemoryPack.MemoryPackIncludeAttribute");
4042
MemoryPackOnSerializingAttribute = GetTypeByMetadataName("MemoryPack.MemoryPackOnSerializingAttribute");

src/MemoryPack.Unity/Assets/Plugins/MemoryPack/Runtime/MemoryPack.Core/Attributes.cs

+7
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,13 @@ public abstract class MemoryPackCustomFormatterAttribute<T> : Attribute
8686
public abstract IMemoryPackFormatter<T> GetFormatter();
8787
}
8888

89+
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = false, Inherited = false)]
90+
public abstract class MemoryPackCustomFormatterAttribute<TFormatter, T> : Attribute
91+
where TFormatter : IMemoryPackFormatter<T>
92+
{
93+
public abstract TFormatter GetFormatter();
94+
}
95+
8996
#endif
9097

9198
// similar naming as System.Text.Json attribtues

src/MemoryPack.Unity/Assets/Plugins/MemoryPack/Runtime/MemoryPack.Core/CustomFormatterAttributes.cs

+14-6
Original file line numberDiff line numberDiff line change
@@ -13,32 +13,40 @@ namespace MemoryPack {
1313

1414
#if !UNITY_2021_2_OR_NEWER
1515

16-
public sealed class Utf8StringFormatterAttribute : MemoryPackCustomFormatterAttribute<string>
16+
public sealed class Utf8StringFormatterAttribute : MemoryPackCustomFormatterAttribute<Utf8StringFormatter, string>
1717
{
18-
public override IMemoryPackFormatter<string> GetFormatter()
18+
public override Utf8StringFormatter GetFormatter()
1919
{
2020
return Utf8StringFormatter.Default;
2121
}
2222
}
2323

24-
public sealed class Utf16StringFormatterAttribute : MemoryPackCustomFormatterAttribute<string>
24+
public sealed class Utf16StringFormatterAttribute : MemoryPackCustomFormatterAttribute<Utf16StringFormatter, string>
2525
{
26-
public override IMemoryPackFormatter<string> GetFormatter()
26+
public override Utf16StringFormatter GetFormatter()
2727
{
2828
return Utf16StringFormatter.Default;
2929
}
3030
}
3131

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

36-
public override IMemoryPackFormatter<Dictionary<string, TValue?>> GetFormatter()
36+
public override DictionaryFormatter<string, TValue?> GetFormatter()
3737
{
3838
return formatter;
3939
}
4040
}
4141

42+
public sealed class InternStringFormatterAttribute : MemoryPackCustomFormatterAttribute<InternStringFormatter, string>
43+
{
44+
public override InternStringFormatter GetFormatter()
45+
{
46+
return InternStringFormatter.Default;
47+
}
48+
}
49+
4250
#endif
4351

4452
}

src/MemoryPack.Unity/Assets/Plugins/MemoryPack/Runtime/MemoryPack.Core/Formatters/StringFormatter.cs

+32
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
#nullable enable
1010
using MemoryPack.Internal;
11+
using System.Runtime.CompilerServices;
1112

1213
namespace MemoryPack.Formatters {
1314

@@ -35,12 +36,14 @@ public sealed class Utf8StringFormatter : MemoryPackFormatter<string>
3536
public static readonly Utf8StringFormatter Default = new Utf8StringFormatter();
3637

3738
[Preserve]
39+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
3840
public override void Serialize(ref MemoryPackWriter writer, ref string? value)
3941
{
4042
writer.WriteUtf8(value);
4143
}
4244

4345
[Preserve]
46+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
4447
public override void Deserialize(ref MemoryPackReader reader, ref string? value)
4548
{
4649
value = reader.ReadString();
@@ -53,16 +56,45 @@ public sealed class Utf16StringFormatter : MemoryPackFormatter<string>
5356
public static readonly Utf16StringFormatter Default = new Utf16StringFormatter();
5457

5558
[Preserve]
59+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
5660
public override void Serialize(ref MemoryPackWriter writer, ref string? value)
5761
{
5862
writer.WriteUtf16(value);
5963
}
6064

6165
[Preserve]
66+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
6267
public override void Deserialize(ref MemoryPackReader reader, ref string? value)
6368
{
6469
value = reader.ReadString();
6570
}
6671
}
6772

73+
[Preserve]
74+
public sealed class InternStringFormatter : MemoryPackFormatter<string>
75+
{
76+
public static readonly InternStringFormatter Default = new InternStringFormatter();
77+
78+
[Preserve]
79+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
80+
public override void Serialize(ref MemoryPackWriter writer, ref string? value)
81+
{
82+
writer.WriteString(value);
83+
}
84+
85+
[Preserve]
86+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
87+
public override void Deserialize(ref MemoryPackReader reader, ref string? value)
88+
{
89+
var str = reader.ReadString();
90+
if (str == null)
91+
{
92+
value = null;
93+
return;
94+
}
95+
96+
value = string.Intern(str);
97+
}
98+
}
99+
68100
}

tests/MemoryPack.Tests/StringTest.cs

+35-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
using System.Runtime.CompilerServices;
1+
using System;
2+
using System.Runtime.CompilerServices;
23
using System.Runtime.InteropServices;
34

45
namespace MemoryPack.Tests;
@@ -41,6 +42,38 @@ public void MalformedUtf8()
4142

4243
Unsafe.WriteUnaligned(ref Unsafe.Add(ref head, 4), 9999);
4344

44-
Assert.Throws<MemoryPackSerializationException>(()=> MemoryPackSerializer.Deserialize<string>(bin));
45+
Assert.Throws<MemoryPackSerializationException>(() => MemoryPackSerializer.Deserialize<string>(bin));
4546
}
47+
48+
[Fact]
49+
public void Intern()
50+
{
51+
var bin = MemoryPackSerializer.Serialize(Guid.NewGuid().ToString());
52+
53+
var str1 = MemoryPackSerializer.Deserialize<string>(bin);
54+
var str2 = MemoryPackSerializer.Deserialize<string>(bin);
55+
56+
str1.Should().Be(str2);
57+
object.ReferenceEquals(str1, str2).Should().BeFalse();
58+
59+
var value = new InternStringTest { Foo = Guid.NewGuid().ToString() };
60+
61+
var bin2 = MemoryPackSerializer.Serialize(value);
62+
63+
var v1 = MemoryPackSerializer.Deserialize<InternStringTest>(bin2)!;
64+
var v2 = MemoryPackSerializer.Deserialize<InternStringTest>(bin2)!;
65+
66+
v1.Foo.Should().Be(v2.Foo);
67+
object.ReferenceEquals(v1.Foo, v2.Foo).Should().BeTrue();
68+
69+
string.IsInterned(v1.Foo!).Should().NotBeNull();
70+
}
71+
}
72+
73+
74+
[MemoryPackable]
75+
public partial class InternStringTest
76+
{
77+
[InternStringFormatter]
78+
public string? Foo { get; set; }
4679
}

0 commit comments

Comments
 (0)