Skip to content

Commit e80031a

Browse files
perf(serialization): optimize polymorphic handling with static wrappers
Performance improvements for polymorphic serialization/deserialization: **Core optimizations:** - Replace lambda closures with static generic wrapper classes for better JIT inlining - Inline polymorphic slow paths to reduce call overhead - Add exact type match fast path in deserializer before cache lookup - Use tuple deconstruction for atomic cache updates **Type safety:** - Add generic constraints 'where TSub : T/TBase' throughout the codebase - Ensure compile-time type safety for subtype relationships **Dictionary generator:** - Move empty count check earlier to avoid unnecessary field access - Optimize KVP serialization with Unsafe.As ref reinterpretation - Remove redundant _count field access These changes maintain Unity compatibility while improving performance through better inlining potential and reduced delegate overhead. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 406b6b9 commit e80031a

4 files changed

Lines changed: 80 additions & 83 deletions

File tree

src/Nino.Core/NinoDeserializer.cs

Lines changed: 43 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,7 @@ public static void DeserializeRefBoxed(ref object val, ref Reader reader, Type t
156156
[SuppressMessage("ReSharper", "StaticMemberInGenericType")]
157157
public static class CachedDeserializer<T>
158158
{
159+
private static int _typeId = -1;
159160
private static DeserializeDelegate<T> _deserializer;
160161
private static DeserializeDelegateRef<T> _deserializerRef;
161162
private static readonly FastMap<int, DeserializeDelegate<T>> SubTypeDeserializers = new();
@@ -184,6 +185,7 @@ public static class CachedDeserializer<T>
184185
public static void SetDeserializer(int typeId, DeserializeDelegate<T> deserializer,
185186
DeserializeDelegateRef<T> deserializerRef)
186187
{
188+
_typeId = typeId;
187189
_deserializer = deserializer;
188190
_deserializerRef = deserializerRef;
189191
SubTypeDeserializers.Add(typeId, _deserializer);
@@ -192,24 +194,40 @@ public static void SetDeserializer(int typeId, DeserializeDelegate<T> deserializ
192194

193195
public static void AddSubTypeDeserializer<TSub>(int subTypeId,
194196
DeserializeDelegate<TSub> deserializer,
195-
DeserializeDelegateRef<TSub> deserializerRef)
197+
DeserializeDelegateRef<TSub> deserializerRef) where TSub : T
196198
{
197-
SubTypeDeserializers.Add(subTypeId, (out T value, ref Reader reader) =>
199+
// Use static generic helper classes to create inlineable wrappers
200+
SubTypeDeserializerWrapper<TSub>.OutDeserializer = deserializer;
201+
SubTypeDeserializerWrapper<TSub>.RefDeserializer = deserializerRef;
202+
SubTypeDeserializers.Add(subTypeId, SubTypeDeserializerWrapper<TSub>.DeserializeOutWrapper);
203+
SubTypeDeserializerRefs.Add(subTypeId, SubTypeDeserializerWrapper<TSub>.DeserializeRefWrapper);
204+
}
205+
206+
// Static wrapper class per TSub - allows better inlining than lambda
207+
private static class SubTypeDeserializerWrapper<TSub> where TSub : T
208+
{
209+
public static DeserializeDelegate<TSub> OutDeserializer;
210+
public static DeserializeDelegateRef<TSub> RefDeserializer;
211+
212+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
213+
public static void DeserializeOutWrapper(out T value, ref Reader reader)
198214
{
199-
deserializer(out TSub subValue, ref reader);
200-
value = subValue is T val ? val : default;
201-
});
202-
SubTypeDeserializerRefs.Add(subTypeId, (ref T value, ref Reader reader) =>
215+
OutDeserializer(out TSub subValue, ref reader);
216+
value = subValue;
217+
}
218+
219+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
220+
public static void DeserializeRefWrapper(ref T value, ref Reader reader)
203221
{
204222
if (value is TSub val)
205223
{
206-
deserializerRef(ref val, ref reader);
224+
RefDeserializer(ref val, ref reader);
207225
}
208226
else
209227
{
210-
throw new Exception($"Cannot cast {value.GetType().FullName} to {typeof(T).FullName}");
228+
ThrowInvalidCast(value?.GetType());
211229
}
212-
});
230+
}
213231
}
214232

215233
// ULTRA-OPTIMIZED: Single boxed method with aggressive inlining and branch elimination
@@ -309,6 +327,13 @@ private static void DeserializePolymorphic(out T value, ref Reader reader)
309327
return;
310328
}
311329

330+
// Fast path: exact type match
331+
if (typeId == _typeId)
332+
{
333+
_deserializer(out value, ref reader);
334+
return;
335+
}
336+
312337
// Fast path: inline cache hit (most common case for batched data)
313338
if (typeId == _cachedTypeIdOut)
314339
{
@@ -317,17 +342,10 @@ private static void DeserializePolymorphic(out T value, ref Reader reader)
317342
}
318343

319344
// Slow path: lookup and update cache
320-
DeserializePolymorphicSlow(out value, ref reader, typeId);
321-
}
322-
323-
[MethodImpl(MethodImplOptions.NoInlining)]
324-
private static void DeserializePolymorphicSlow(out T value, ref Reader reader, int typeId)
325-
{
326345
// Handle subtype with single lookup
327346
if (SubTypeDeserializers.TryGetValue(typeId, out var subTypeDeserializer))
328347
{
329-
_cachedTypeIdOut = typeId;
330-
_cachedDeserializer = subTypeDeserializer;
348+
(_cachedTypeIdOut, _cachedDeserializer) = (typeId, subTypeDeserializer);
331349
subTypeDeserializer(out value, ref reader);
332350
return;
333351
}
@@ -348,6 +366,13 @@ private static void DeserializeRefPolymorphic(ref T value, ref Reader reader)
348366
return;
349367
}
350368

369+
// Fast path: exact type match
370+
if (typeId == _typeId)
371+
{
372+
_deserializerRef(ref value, ref reader);
373+
return;
374+
}
375+
351376
// Fast path: inline cache hit (most common case for batched data)
352377
if (typeId == _cachedTypeIdRef)
353378
{
@@ -356,17 +381,10 @@ private static void DeserializeRefPolymorphic(ref T value, ref Reader reader)
356381
}
357382

358383
// Slow path: lookup and update cache
359-
DeserializeRefPolymorphicSlow(ref value, ref reader, typeId);
360-
}
361-
362-
[MethodImpl(MethodImplOptions.NoInlining)]
363-
private static void DeserializeRefPolymorphicSlow(ref T value, ref Reader reader, int typeId)
364-
{
365384
// Handle subtype deserialization
366385
if (SubTypeDeserializerRefs.TryGetValue(typeId, out var subTypeDeserializer))
367386
{
368-
_cachedTypeIdRef = typeId;
369-
_cachedDeserializerRef = subTypeDeserializer;
387+
(_cachedTypeIdRef, _cachedDeserializerRef) = (typeId, subTypeDeserializer);
370388
subTypeDeserializer(ref value, ref reader);
371389
return;
372390
}

src/Nino.Core/NinoSerializer.cs

Lines changed: 25 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -176,57 +176,39 @@ public static class CachedSerializer<T>
176176
private static void ThrowInvalidCast(Type actualType) =>
177177
throw new InvalidCastException($"Cannot cast {actualType?.FullName ?? "null"} to {typeof(T).FullName}");
178178

179-
public static void AddSubTypeSerializer<TSub>(SerializeDelegate<TSub> serializer)
179+
public static void AddSubTypeSerializer<TSub>(SerializeDelegate<TSub> serializer) where TSub : T
180180
{
181-
if (typeof(TSub).IsValueType)
182-
{
183-
// cast T to TSub via boxing, T here must be interface, then add to the map
184-
SubTypeSerializers.Add(typeof(TSub).TypeHandle.Value, (T val, ref Writer writer) =>
185-
{
186-
if (val is TSub sub)
187-
{
188-
// Fast path: already the correct type
189-
serializer(sub, ref writer);
190-
return;
191-
}
181+
// Use a static generic helper class to create an inlineable wrapper
182+
SubTypeSerializerWrapper<TSub>.SubSerializer = serializer;
183+
SubTypeSerializers.Add(typeof(TSub).TypeHandle.Value, SubTypeSerializerWrapper<TSub>.SerializeWrapper);
184+
}
192185

193-
ThrowInvalidCast(val?.GetType());
194-
});
195-
}
196-
else
186+
// Static wrapper class per TSub - allows better inlining than lambda
187+
private static class SubTypeSerializerWrapper<TSub> where TSub : T
188+
{
189+
public static SerializeDelegate<TSub> SubSerializer;
190+
191+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
192+
public static void SerializeWrapper(T val, ref Writer writer)
197193
{
198-
// simply cast TSub to T directly, then add to the map
199-
SubTypeSerializers.Add(typeof(TSub).TypeHandle.Value, (T val, ref Writer writer) =>
194+
if (val is TSub sub)
195+
{
196+
// This can be inlined by JIT since it's a static method call with known target
197+
SubSerializer(sub, ref writer);
198+
}
199+
else
200200
{
201-
switch (val)
202-
{
203-
case null:
204-
// Handle null case
205-
serializer(default, ref writer);
206-
return;
207-
case TSub sub:
208-
// Fast path: already the correct type
209-
serializer(sub, ref writer);
210-
return;
211-
}
212-
213-
ThrowInvalidCast(val.GetType());
214-
});
201+
ThrowInvalidCast(val?.GetType());
202+
}
215203
}
216204
}
217205

218206
[MethodImpl(MethodImplOptions.AggressiveInlining)]
219207
public static void SerializeBoxed(object value, ref Writer writer)
220208
{
221-
switch (value)
222-
{
223-
case null:
224-
Serializer(default, ref writer);
225-
break;
226-
case T val:
227-
Serializer(val, ref writer);
228-
break;
229-
}
209+
if (value == null)
210+
Serializer(default, ref writer);
211+
else if (value is T val) Serializer(val, ref writer);
230212
}
231213

232214
// ULTRA-OPTIMIZED: Single core method with all paths optimized
@@ -289,18 +271,11 @@ private static unsafe void SerializePolymorphic(T val, ref Writer writer)
289271
return;
290272
}
291273

292-
// SLOW PATH: Dictionary lookup and cache update
293-
SerializePolymorphicSlow(val, ref writer, actualTypeHandle);
294-
}
295-
296-
[MethodImpl(MethodImplOptions.NoInlining)]
297-
private static void SerializePolymorphicSlow(T val, ref Writer writer, IntPtr actualTypeHandle)
298-
{
274+
// SLOW PATH: Full lookup in subtype map
299275
// Handle subtype serialization
300276
if (SubTypeSerializers.TryGetValue(actualTypeHandle, out var subTypeSerializer))
301277
{
302-
_cachedTypeHandle = actualTypeHandle;
303-
_cachedSerializer = subTypeSerializer;
278+
(_cachedTypeHandle, _cachedSerializer) = (actualTypeHandle, subTypeSerializer);
304279
subTypeSerializer(val, ref writer);
305280
return;
306281
}

src/Nino.Core/NinoTypeMetadata.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,7 @@ public static bool HasBaseType(Type type)
100100

101101
[EditorBrowsable(EditorBrowsableState.Never)]
102102
public static void RecordSubTypeSerializer<TBase, TSub>(SerializeDelegate<TSub> subTypeSerializer)
103+
where TSub : TBase
103104
{
104105
lock (SubTypeSerializerRegistration<TBase, TSub>.Lock)
105106
{
@@ -122,6 +123,7 @@ private static class SubTypeSerializerRegistration<TBase, TSub>
122123
public static void RecordSubTypeDeserializer<TBase, TSub>(int subTypeId,
123124
DeserializeDelegate<TSub> subTypeDeserializer,
124125
DeserializeDelegateRef<TSub> subTypeDeserializerRef)
126+
where TSub : TBase
125127
{
126128
lock (SubTypeDeserializerRegistration<TBase, TSub>.Lock)
127129
{

src/Nino.Generator/BuiltInType/DictionaryGenerator.cs

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,11 @@ protected override void GenerateSerializer(ITypeSymbol typeSymbol, Writer writer
9191
writer.AppendLine(" int cnt = value.Count;");
9292
writer.AppendLine(" writer.Write(TypeCollector.GetCollectionHeader(cnt));");
9393
writer.AppendLine();
94+
writer.AppendLine(" if (cnt == 0)");
95+
writer.AppendLine(" {");
96+
writer.AppendLine(" return;");
97+
writer.AppendLine(" }");
98+
writer.AppendLine();
9499

95100
var originalDef = namedType.OriginalDefinition.ToDisplayString();
96101
bool isDictionary = originalDef == "System.Collections.Generic.Dictionary<TKey, TValue>";
@@ -108,19 +113,14 @@ protected override void GenerateSerializer(ITypeSymbol typeSymbol, Writer writer
108113
writer.AppendLine(" {");
109114
writer.AppendLine(" return;");
110115
writer.AppendLine(" }");
111-
writer.AppendLine(" int count = dict._count;");
112-
writer.AppendLine(" if (count == 0)");
113-
writer.AppendLine(" {");
114-
writer.AppendLine(" return;");
115-
writer.AppendLine(" }");
116116
writer.AppendLine(" // Iterate entries via direct ref to avoid bounds checks");
117117
writer.AppendLine("#if !UNITY_2020_2_OR_NEWER");
118118
writer.AppendLine(" ref var entryRef = ref System.Runtime.InteropServices.MemoryMarshal.GetArrayDataReference(entries);");
119119
writer.AppendLine("#else");
120120
writer.AppendLine(" ref var entryRef = ref entries[0];");
121121
writer.AppendLine("#endif");
122122
writer.AppendLine(" int index = 0;");
123-
writer.AppendLine(" while ((uint)index < (uint)count)");
123+
writer.AppendLine(" while ((uint)index < (uint)cnt)");
124124
writer.AppendLine(" {");
125125
writer.AppendLine(" ref var entry = ref System.Runtime.CompilerServices.Unsafe.Add(ref entryRef, index++);");
126126
writer.AppendLine(" if (entry.next < -1)");
@@ -130,11 +130,13 @@ protected override void GenerateSerializer(ITypeSymbol typeSymbol, Writer writer
130130

131131
if (kvpIsUnmanaged)
132132
{
133-
writer.Append(" var kvp = new System.Collections.Generic.KeyValuePair<");
133+
writer.Append(" ref var kvp = ref System.Runtime.CompilerServices.Unsafe.As<");
134+
writer.Append(keyType.GetDisplayString());
135+
writer.Append(", System.Collections.Generic.KeyValuePair<");
134136
writer.Append(keyType.GetDisplayString());
135137
writer.Append(", ");
136138
writer.Append(valueType.GetDisplayString());
137-
writer.AppendLine(">(entry.key, entry.value);");
139+
writer.AppendLine(">>(ref entry.key);");
138140
writer.AppendLine(" writer.UnsafeWrite(kvp);");
139141
}
140142
else

0 commit comments

Comments
 (0)