Skip to content

Commit 672c2c3

Browse files
perf(serializer): optimize polymorphic type resolution with multiple strategies
- Add sealed type optimization: skip polymorphic path entirely for sealed types (JIT-eliminated) - Reorder type checks: verify inline cache before base type for batched data - Implement double-pointer dereference for .NET 5.0+: read RuntimeTypeHandle.Value directly from object header to avoid expensive GetType() virtual call - Apply sealed type optimization to both serializer and deserializer Performance improvements: - Sealed types: zero GetType() overhead - Batched homogeneous data: inline cache hits after first item - .NET 5.0+: no GetType() call for polymorphic serialization 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 9bb574b commit 672c2c3

2 files changed

Lines changed: 40 additions & 24 deletions

File tree

src/Nino.Core/NinoDeserializer.cs

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,9 @@ public static class CachedDeserializer<T>
174174
// ReSharper disable once StaticMemberInGenericType
175175
private static readonly bool HasBaseType = NinoTypeMetadata.HasBaseType(typeof(T));
176176

177+
// ReSharper disable once StaticMemberInGenericType
178+
private static readonly bool IsSealed = typeof(T).IsSealed || typeof(T).IsValueType;
179+
177180
// ULTIMATE: JIT-eliminated constants for maximum performance
178181
// ReSharper disable once StaticMemberInGenericType
179182
internal static readonly bool IsSimpleType = !IsReferenceOrContainsReferences && !HasBaseType;
@@ -254,11 +257,12 @@ public static void Deserialize(out T value, ref Reader reader)
254257
return;
255258
}
256259

257-
// OPTIMIZED: Direct deserialization with polymorphism support
258-
// Common case first: no subtypes (Count == 1 means only base type registered)
259-
if (SubTypeDeserializers.Count == 1)
260+
// FAST PATH 2: JIT-eliminated branch for sealed types
261+
// If T is sealed or a value type, it CANNOT have a different runtime type
262+
// This completely eliminates polymorphic deserialization overhead
263+
if (IsSealed || SubTypeDeserializers.Count == 1)
260264
{
261-
// DIRECT DELEGATE: Generated code path - no null check needed
265+
// DIRECT DELEGATE: Generated code path - no polymorphism possible
262266
_deserializer(out value, ref reader);
263267
}
264268
else
@@ -278,11 +282,12 @@ public static void DeserializeRef(ref T value, ref Reader reader)
278282
return;
279283
}
280284

281-
// OPTIMIZED: Direct deserialization with polymorphism support
282-
// Common case first: no subtypes (Count == 1 means only base type registered)
283-
if (SubTypeDeserializerRefs.Count == 1)
285+
// FAST PATH 2: JIT-eliminated branch for sealed types
286+
// If T is sealed or a value type, it CANNOT have a different runtime type
287+
// This completely eliminates polymorphic deserialization overhead
288+
if (IsSealed || SubTypeDeserializerRefs.Count == 1)
284289
{
285-
// DIRECT DELEGATE: Generated code path - no null check needed
290+
// DIRECT DELEGATE: Generated code path - no polymorphism possible
286291
_deserializerRef(ref value, ref reader);
287292
}
288293
else

src/Nino.Core/NinoSerializer.cs

Lines changed: 27 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,7 @@ public static void SerializeBoxed(object value, ref Writer writer, Type type)
140140
}
141141

142142
public delegate void SerializeDelegate<TVal>(TVal value, ref Writer writer);
143+
143144
public delegate void SerializeDelegateBoxed(object value, ref Writer writer);
144145

145146

@@ -164,14 +165,17 @@ public static class CachedSerializer<T>
164165
// ReSharper disable once StaticMemberInGenericType
165166
internal static readonly bool HasBaseType = NinoTypeMetadata.HasBaseType(typeof(T));
166167

168+
// ReSharper disable once StaticMemberInGenericType
169+
private static readonly bool IsSealed = typeof(T).IsSealed || typeof(T).IsValueType;
170+
167171
// ULTIMATE: JIT-eliminated constant for maximum performance
168172
// ReSharper disable once StaticMemberInGenericType
169173
internal static readonly bool IsSimpleType = !IsReferenceOrContainsReferences && !HasBaseType;
170174

171175
[MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.NoOptimization)] // Cold exception path
172176
private static void ThrowInvalidCast(Type actualType) =>
173177
throw new InvalidCastException($"Cannot cast {actualType?.FullName ?? "null"} to {typeof(T).FullName}");
174-
178+
175179
public static void AddSubTypeSerializer<TSub>(SerializeDelegate<TSub> serializer)
176180
{
177181
if (typeof(TSub).IsValueType)
@@ -236,11 +240,12 @@ public static void Serialize(T val, ref Writer writer)
236240
return;
237241
}
238242

239-
// OPTIMIZED: Direct serialization with polymorphism support
240-
// Common case first: no subtypes
241-
if (SubTypeSerializers.Count == 0)
243+
// FAST PATH 2: JIT-eliminated branch for sealed types
244+
// If T is sealed or a value type, it CANNOT have a different runtime type
245+
// This completely eliminates the need for GetType() calls
246+
if (IsSealed || SubTypeSerializers.Count == 0)
242247
{
243-
// DIRECT DELEGATE: Generated code path - no null check needed
248+
// DIRECT DELEGATE: Generated code path - no polymorphism possible
244249
Serializer(val, ref writer);
245250
}
246251
else
@@ -250,31 +255,37 @@ public static void Serialize(T val, ref Writer writer)
250255
}
251256

252257
[MethodImpl(MethodImplOptions.AggressiveInlining)]
253-
private static void SerializePolymorphic(T val, ref Writer writer)
258+
private static unsafe void SerializePolymorphic(T val, ref Writer writer)
254259
{
255260
if (val == null)
256261
{
257262
writer.Write(TypeCollector.Null);
258263
return;
259264
}
260265

261-
// OPTIMIZATION: Use EqualityComparer's cached type handle when possible
262-
// For reference types, GetType() is still needed but the inline cache minimizes calls
266+
// Get type handle - optimized for .NET 5.0+ to avoid expensive GetType() call
267+
// Read RuntimeTypeHandle.Value directly from object header via double pointer dereference
268+
#if NET5_0_OR_GREATER
269+
// Object header at offset 0 contains a pointer to the type handle
270+
// Double dereference: first * gets the pointer, second * gets the RuntimeTypeHandle.Value
271+
IntPtr actualTypeHandle = **(IntPtr**)Unsafe.AsPointer(ref val);
272+
#else
273+
// Fallback to GetType() for older runtimes
263274
IntPtr actualTypeHandle = val.GetType().TypeHandle.Value;
275+
#endif
264276

265-
// FAST PATH 1: Check if it's the base type first (most common case)
266-
// This check is very cheap - just pointer comparison
267-
if (actualTypeHandle == TypeHandle)
277+
// FAST PATH 1: Inline cache hit (most common for homogeneous batches)
278+
// Check this FIRST - single pointer comparison, very cheap
279+
if (actualTypeHandle == _cachedTypeHandle)
268280
{
269-
Serializer(val, ref writer);
281+
_cachedSerializer(val, ref writer);
270282
return;
271283
}
272284

273-
// FAST PATH 2: Inline cache hit (most common case for batched homogeneous data)
274-
// Single comparison - avoids dictionary lookup
275-
if (actualTypeHandle == _cachedTypeHandle)
285+
// FAST PATH 2: Base type (common for non-polymorphic usage)
286+
if (actualTypeHandle == TypeHandle)
276287
{
277-
_cachedSerializer(val, ref writer);
288+
Serializer(val, ref writer);
278289
return;
279290
}
280291

0 commit comments

Comments
 (0)