Skip to content

Commit ee49724

Browse files
perf(generator): optimize polymorphic serialization with compile-time code generation
- Generate SerializePolymorphic methods for non-sealed types with switch-based dispatch - Inline type constant writes and member serialization in each case branch - Add indent parameter to WriteMembers for proper code formatting at any nesting level - Route polymorphic types to generated methods instead of runtime CachedSerializer lookup - Eliminate per-instance type caching overhead by baking polymorphism into generated code Co-Authored-By: Claude <noreply@anthropic.com>
1 parent ea65c83 commit ee49724

6 files changed

Lines changed: 119 additions & 34 deletions

File tree

src/Nino.Core/NinoSerializer.cs

Lines changed: 27 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -162,7 +162,8 @@ public static void SerializeBoxed(object value, ref Writer writer, Type type)
162162
[SuppressMessage("ReSharper", "StaticMemberInGenericType")]
163163
public static class CachedSerializer<T>
164164
{
165-
public static SerializeDelegate<T> Serializer;
165+
private static SerializeDelegate<T> _optimalSerializer;
166+
private static SerializeDelegate<T> _serializer;
166167
public static readonly FastMap<IntPtr, SerializeDelegate<T>> SubTypeSerializers = new();
167168

168169
// Cache expensive type checks
@@ -182,6 +183,12 @@ public static class CachedSerializer<T>
182183
// ReSharper disable once StaticMemberInGenericType
183184
internal static readonly bool IsSimpleType = !IsReferenceOrContainsReferences && !HasBaseType;
184185

186+
public static void SetSerializer(SerializeDelegate<T> serializer, SerializeDelegate<T> optimalSerializer)
187+
{
188+
_serializer = serializer;
189+
_optimalSerializer = optimalSerializer;
190+
}
191+
185192
public static void AddSubTypeSerializer<TSub>(SerializeDelegate<TSub> serializer) where TSub : T
186193
{
187194
// Use a static generic helper class to create an inlineable wrapper
@@ -214,8 +221,8 @@ public static void SerializeWrapper(T val, ref Writer writer)
214221
public static void SerializeBoxed(object value, ref Writer writer)
215222
{
216223
if (value == null)
217-
Serializer(default, ref writer);
218-
else if (value is T val) Serializer(val, ref writer);
224+
_serializer(default, ref writer);
225+
else if (value is T val) _serializer(val, ref writer);
219226
}
220227

221228
// ULTRA-OPTIMIZED: Single core method with all paths optimized
@@ -229,23 +236,30 @@ public static void Serialize(T val, ref Writer writer)
229236
return;
230237
}
231238

239+
// FAST PATH 1: Optimal serializer for polymorphic usage (with subtypes)
240+
// This is a pre-generated serializer that handles polymorphism internally
241+
if (_optimalSerializer != null)
242+
{
243+
_optimalSerializer(val, ref writer);
244+
return;
245+
}
246+
247+
SerializePolymorphic(val, ref writer);
248+
}
249+
250+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
251+
public static unsafe void SerializePolymorphic(T val, ref Writer writer)
252+
{
232253
// FAST PATH 2: JIT-eliminated branch for sealed types
233254
// If T is sealed or a value type, it CANNOT have a different runtime type
234255
// This completely eliminates the need for GetType() calls
235256
if (IsSealed || SubTypeSerializers.Count == 0)
236257
{
237258
// DIRECT DELEGATE: Generated code path - no polymorphism possible
238-
Serializer(val, ref writer);
239-
}
240-
else
241-
{
242-
SerializePolymorphic(val, ref writer);
259+
_serializer(val, ref writer);
260+
return;
243261
}
244-
}
245262

246-
[MethodImpl(MethodImplOptions.AggressiveInlining)]
247-
private static unsafe void SerializePolymorphic(T val, ref Writer writer)
248-
{
249263
if (val == null)
250264
{
251265
writer.Write(TypeCollector.Null);
@@ -266,7 +280,7 @@ private static unsafe void SerializePolymorphic(T val, ref Writer writer)
266280
// FAST PATH: Base type (common for non-polymorphic usage)
267281
if (actualTypeHandle == TypeHandle)
268282
{
269-
Serializer!(val, ref writer);
283+
_serializer!(val, ref writer);
270284
return;
271285
}
272286

src/Nino.Core/NinoTypeMetadata.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,8 @@ public static class NinoTypeMetadata
2626
DeserializeDelegateRefBoxed refOverload)> TypeIdToDeserializer = new();
2727

2828
[EditorBrowsable(EditorBrowsableState.Never)]
29-
public static void RegisterSerializer<T>(SerializeDelegate<T> serializer, bool hasBaseType)
29+
public static void RegisterSerializer<T>(SerializeDelegate<T> serializer,
30+
SerializeDelegate<T> optimalSerializer, bool hasBaseType)
3031
{
3132
lock (SerializerRegistration<T>.Lock)
3233
{
@@ -41,7 +42,7 @@ public static void RegisterSerializer<T>(SerializeDelegate<T> serializer, bool h
4142
HasBaseTypeMap.Add(typeHandle, true);
4243
}
4344

44-
CachedSerializer<T>.Serializer = serializer;
45+
CachedSerializer<T>.SetSerializer(serializer, optimalSerializer);
4546
Serializers.Add(typeHandle, CachedSerializer<T>.SerializeBoxed);
4647

4748
SerializerRegistration<T>.Registered = true;

src/Nino.Generator/BuiltInType/NinoBuiltInTypesGenerator.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@ protected override void Generate(SourceProductionContext spc)
109109
{
110110
var typeName = registeredType.GetDisplayString();
111111
registrationCode.AppendLine(
112-
$" NinoTypeMetadata.RegisterSerializer<{typeName}>(Serializer.Serialize, false);");
112+
$" NinoTypeMetadata.RegisterSerializer<{typeName}>(Serializer.Serialize, Serializer.Serialize, false);");
113113
registrationCode.AppendLine(
114114
$" NinoTypeMetadata.RegisterDeserializer<{typeName}>(-1, Deserializer.Deserialize, Deserializer.DeserializeRef, false);");
115115
}

src/Nino.Generator/Common/SerializerGenerator.Trivial.cs

Lines changed: 77 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,62 @@ private void GenerateTrivialCode(SourceProductionContext spc, HashSet<ITypeSymbo
2525
if (!generatedTypeNames.Add(ninoType.TypeSymbol.GetDisplayString()))
2626
continue;
2727

28+
29+
if (!ninoType.TypeSymbol.IsSealedOrStruct())
30+
{
31+
sb.AppendLine();
32+
sb.AppendLine($$"""
33+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
34+
public static void SerializePolymorphic({{ninoType.TypeSymbol.GetTypeFullName()}} value, ref Writer writer)
35+
{
36+
switch(value)
37+
{
38+
case null:
39+
{
40+
writer.Write(TypeCollector.Null);
41+
return;
42+
}
43+
""");
44+
if (NinoGraph.SubTypes.TryGetValue(ninoType, out var subTypes))
45+
{
46+
foreach (var subType in subTypes)
47+
{
48+
if (!subType.TypeSymbol.IsInstanceType())
49+
continue;
50+
51+
var valName = subType.TypeSymbol.GetCachedVariableName("val_");
52+
sb.AppendLine($$"""
53+
case {{subType.TypeSymbol.GetTypeFullName()}} {{valName}}:
54+
{
55+
""");
56+
57+
sb.AppendLine(
58+
$" writer.Write(NinoTypeConst.{subType.TypeSymbol.GetTypeFullName().GetTypeConstName()});");
59+
60+
// Optimized write path - direct write for unmanaged types
61+
if (subType.TypeSymbol.IsUnmanagedType)
62+
{
63+
sb.AppendLine($" writer.Write({valName});");
64+
}
65+
else
66+
{
67+
WriteMembers(subType, valName, sb, " ");
68+
}
69+
70+
sb.AppendLine(" return;");
71+
sb.AppendLine(" }");
72+
}
73+
}
74+
75+
sb.AppendLine(" default:");
76+
sb.AppendLine(
77+
$" CachedSerializer<{ninoType.TypeSymbol.GetTypeFullName()}>.SerializePolymorphic(value, ref writer);");
78+
sb.AppendLine(" break;");
79+
sb.AppendLine(" }");
80+
sb.AppendLine(" }");
81+
sb.AppendLine();
82+
}
83+
2884
if (!ninoType.TypeSymbol.IsInstanceType() ||
2985
!string.IsNullOrEmpty(ninoType.CustomSerializer))
3086
continue;
@@ -206,7 +262,7 @@ private bool TryGetInlineSerializeCall(ITypeSymbol type, string valueExpression,
206262
return true;
207263
}
208264

209-
private void WriteMembers(NinoType type, string valName, StringBuilder sb)
265+
private void WriteMembers(NinoType type, string valName, StringBuilder sb, string indent = "")
210266
{
211267
// First pass: collect all types that need serializers or custom formatters
212268
HashSet<ITypeSymbol> typesNeedingSerializers = new(SymbolEqualityComparer.Default);
@@ -320,7 +376,7 @@ string GetSerializerVarName(ITypeSymbol serializerType)
320376
// PRIORITY 1: Custom formatter (highest priority)
321377
if (customFormatterVarsByMember.TryGetValue(member, out var formatterVar))
322378
{
323-
sb.AppendLine($" {formatterVar}.Serialize({val}, ref writer);");
379+
sb.AppendLine($"{indent} {formatterVar}.Serialize({val}, ref writer);");
324380
}
325381
}
326382
else
@@ -331,13 +387,13 @@ string GetSerializerVarName(ITypeSymbol serializerType)
331387
{
332388
case NinoTypeHelper.NinoTypeKind.Unmanaged:
333389
// PRIORITY 2: Unmanaged types - write directly
334-
sb.AppendLine($" writer.Write({val});");
390+
sb.AppendLine($"{indent} writer.Write({val});");
335391
break;
336392

337393
case NinoTypeHelper.NinoTypeKind.Boxed:
338394
// PRIORITY 3: Object type - call boxed API in NinoSerializer directly
339395
sb.AppendLine(
340-
$" NinoSerializer.SerializeBoxed({val}, ref writer, {val}?.GetType());");
396+
$"{indent} NinoSerializer.SerializeBoxed({val}, ref writer, {val}?.GetType());");
341397
break;
342398

343399
case NinoTypeHelper.NinoTypeKind.BuiltIn:
@@ -346,16 +402,16 @@ string GetSerializerVarName(ITypeSymbol serializerType)
346402
{
347403
if (member.IsUtf8String)
348404
{
349-
sb.AppendLine($" writer.WriteUtf8({val});");
405+
sb.AppendLine($"{indent} writer.WriteUtf8({val});");
350406
}
351407
else
352408
{
353-
sb.AppendLine($" writer.Write({val});");
409+
sb.AppendLine($"{indent} writer.Write({val});");
354410
}
355411
}
356412
else
357413
{
358-
sb.AppendLine($" Serializer.Serialize({val}, ref writer);");
414+
sb.AppendLine($"{indent} Serializer.Serialize({val}, ref writer);");
359415
}
360416

361417
break;
@@ -364,30 +420,36 @@ string GetSerializerVarName(ITypeSymbol serializerType)
364420
// PRIORITY 5: NinoType - use CachedSerializer
365421
if (TryGetInlineSerializeCall(declaredType, val, out var inlineCall))
366422
{
367-
sb.AppendLine($" {inlineCall};");
423+
sb.AppendLine($"{indent} {inlineCall};");
424+
}
425+
else if (!declaredType.IsSealedOrStruct())
426+
{
427+
sb.AppendLine(
428+
$"{indent} Serializer.SerializePolymorphic({val}, ref writer);");
368429
}
369430
else
370431
{
371432
var serializerVar = GetSerializerVarName(declaredType);
372-
sb.AppendLine($" {serializerVar}.Serialize({val}, ref writer);");
433+
sb.AppendLine($"{indent} {serializerVar}.Serialize({val}, ref writer);");
373434
}
435+
374436
break;
375437
}
376438
}
377439
}
378440
else
379441
{
380442
// Standard path with version tolerance support
381-
sb.AppendLine($"#if {NinoTypeHelper.WeakVersionToleranceSymbol}");
443+
sb.AppendLine($"{indent}#if {NinoTypeHelper.WeakVersionToleranceSymbol}");
382444
foreach (var val in valNames)
383445
{
384-
sb.AppendLine($" writer.Write({val});");
446+
sb.AppendLine($"{indent} writer.Write({val});");
385447
}
386448

387-
sb.AppendLine("#else");
388-
sb.AppendLine($" writer.Write(NinoTuple.Create({string.Join(", ", valNames)}));");
389-
sb.AppendLine("#endif");
449+
sb.AppendLine($"{indent}#else");
450+
sb.AppendLine($"{indent} writer.Write(NinoTuple.Create({string.Join(", ", valNames)}));");
451+
sb.AppendLine($"{indent}#endif");
390452
}
391453
}
392454
}
393-
}
455+
}

src/Nino.Generator/Common/SerializerGenerator.cs

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,8 @@ private void GenerateGenericRegister(StringBuilder sb, string name, HashSet<ITyp
4545
{
4646
var baseTypes = NinoGraph.BaseTypes[ninoType];
4747
string prefix;
48+
string optimal;
49+
4850
foreach (var baseType in baseTypes)
4951
{
5052
if (registeredTypes.Add(baseType.TypeSymbol))
@@ -53,19 +55,23 @@ private void GenerateGenericRegister(StringBuilder sb, string name, HashSet<ITyp
5355
prefix = !string.IsNullOrEmpty(baseType.CustomSerializer)
5456
? $"{baseType.CustomSerializer}."
5557
: "";
58+
optimal = !baseType.TypeSymbol.IsSealedOrStruct() ? $"{prefix}SerializePolymorphic" : "null";
59+
5660
var method = baseType.TypeSymbol.IsInstanceType() ? $"{prefix}SerializeImpl" : "null";
5761
sb.AppendLine($$"""
58-
NinoTypeMetadata.RegisterSerializer<{{baseTypeName}}>({{method}}, {{baseType.Parents.Any().ToString().ToLower()}});
62+
NinoTypeMetadata.RegisterSerializer<{{baseTypeName}}>({{method}}, {{optimal}}, {{baseType.Parents.Any().ToString().ToLower()}});
5963
""");
6064
}
6165
}
6266

6367
prefix = !string.IsNullOrEmpty(ninoType.CustomSerializer) ? $"{ninoType.CustomSerializer}." : "";
68+
optimal = !ninoType.TypeSymbol.IsSealedOrStruct() ? $"{prefix}SerializePolymorphic" : "null";
69+
6470
if (registeredTypes.Add(type))
6571
{
6672
var method = ninoType.TypeSymbol.IsInstanceType() ? $"{prefix}SerializeImpl" : "null";
6773
sb.AppendLine($$"""
68-
NinoTypeMetadata.RegisterSerializer<{{typeFullName}}>({{method}}, {{ninoType.Parents.Any().ToString().ToLower()}});
74+
NinoTypeMetadata.RegisterSerializer<{{typeFullName}}>({{method}}, {{optimal}},{{ninoType.Parents.Any().ToString().ToLower()}});
6975
""");
7076
}
7177

@@ -83,7 +89,7 @@ private void GenerateGenericRegister(StringBuilder sb, string name, HashSet<ITyp
8389

8490
if (registeredTypes.Add(type))
8591
sb.AppendLine($$"""
86-
NinoTypeMetadata.RegisterSerializer<{{typeFullName}}>(Serialize, false);
92+
NinoTypeMetadata.RegisterSerializer<{{typeFullName}}>(Serialize, Serialize, false);
8793
""");
8894
}
8995

src/Nino.Generator/Template/NinoBuiltInTypeGenerator.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,8 @@ protected string GetSerializeString(ITypeSymbol type, string valueName)
137137
case NinoTypeHelper.NinoTypeKind.NinoType:
138138
if (TryGetInlineSerializeCall(type, valueName, out var inlineSerialize))
139139
return inlineSerialize;
140+
if (!type.IsSealedOrStruct())
141+
return $"Serializer.SerializePolymorphic({valueName}, ref writer);";
140142
return $"NinoSerializer.Serialize<{type.GetDisplayString()}>({valueName}, ref writer);";
141143
case NinoTypeHelper.NinoTypeKind.BuiltIn:
142144
return $"Serializer.Serialize({valueName}, ref writer);";

0 commit comments

Comments
 (0)