Improve shared reference handling & benchmark realism

- Test data now controls IId shared ref % for realistic deduplication benchmarks; display names include IId ref ratio.
- Added deep-level clearing of IId refs for realistic object graphs.
- Pallet, Measurement, and Point models now support shared IId refs.
- TestDataFactory passes shared refs to all hierarchy levels.
- Refactored TypeMetadataWrapper for type-specific Id getters, identity maps, and registration—removes hot path type checks/switches.
- AcBinary deserializer now uses new typed methods for reference tracking and registration.
- SerializationContextBase uses pre-cast Id getters for zero-overhead tracking.
- Reduced quick benchmark warmup iterations for faster startup.
- Improves performance, clarity, and maintainability of reference handling and benchmarks.
This commit is contained in:
Loretta 2026-01-24 01:39:30 +01:00
parent 40fb4950a6
commit 6df5c53937
7 changed files with 487 additions and 107 deletions

View File

@ -73,7 +73,7 @@ public static class Program
foreach (var testData in testDataSets)
{
System.Console.WriteLine($"\n{'═'.ToString().PadRight(70, '═')}");
System.Console.WriteLine($"TEST DATA: {testData.Name}");
System.Console.WriteLine($"TEST DATA: {testData.DisplayName}");
System.Console.WriteLine($"{'═'.ToString().PadRight(70, '═')}");
var results = RunBenchmarksForTestData(testData, mode);
@ -83,6 +83,8 @@ public static class Program
// Print grouped results
PrintGroupedResults(allResults, testDataSets);
// Save results to file
SaveResults(allResults, testDataSets);
@ -106,22 +108,44 @@ public static class Program
private static TestDataSet CreateSmallTestData()
{
TestDataFactory.ResetIdCounter();
// Create shared references - IId types (only at Order/Item level)
var sharedTag = TestDataFactory.CreateTag("SharedTag");
var sharedUser = TestDataFactory.CreateUser("shareduser");
var order = TestDataFactory.CreateOrder(
itemCount: 2,
palletsPerItem: 2,
measurementsPerPallet: 2,
pointsPerMeasurement: 2);
pointsPerMeasurement: 2,
sharedTag: sharedTag,
sharedUser: sharedUser);
return new TestDataSet("Small (2x2x2x2)", order);
// Clear deeper level refs for realistic ~10% ratio
ClearDeepLevelRefs(order);
return new TestDataSet("Small (2x2x2x2)", order, iidRefPercent: 10);
}
private static TestDataSet CreateMediumTestData()
{
TestDataFactory.ResetIdCounter();
// IId shared references
var sharedTag = TestDataFactory.CreateTag("SharedTag");
var sharedUser = TestDataFactory.CreateUser("shareduser");
var sharedMeta = TestDataFactory.CreateMetadata("shared", withChild: true);
// Non-IId shared reference - create separate preferences for 2 users
var sharedPreferences = new UserPreferences
{
Theme = "dark",
Language = "en-US",
NotificationsEnabled = true,
EmailDigestFrequency = "weekly"
};
sharedUser.Preferences = sharedPreferences;
var order = TestDataFactory.CreateOrder(
itemCount: 3,
palletsPerItem: 3,
@ -129,37 +153,75 @@ public static class Program
pointsPerMeasurement: 4,
sharedTag: sharedTag,
sharedUser: sharedUser,
sharedMetadata: sharedMeta);
sharedMetadata: sharedMeta,
sharedPreferences: sharedPreferences);
return new TestDataSet("Medium (3x3x3x4, shared refs)", order);
// Clear deeper level refs for realistic ~10% ratio
ClearDeepLevelRefs(order);
return new TestDataSet("Medium (3x3x3x4)", order, iidRefPercent: 10);
}
private static TestDataSet CreateLargeTestData()
{
TestDataFactory.ResetIdCounter();
// IId shared references
var sharedTag = TestDataFactory.CreateTag("SharedTag");
var sharedUser = TestDataFactory.CreateUser("shareduser");
// Non-IId shared reference
var sharedPreferences = new UserPreferences
{
Theme = "light",
Language = "de-DE",
NotificationsEnabled = false,
EmailDigestFrequency = "daily"
};
sharedUser.Preferences = sharedPreferences;
var order = TestDataFactory.CreateOrder(
itemCount: 5,
palletsPerItem: 5,
measurementsPerPallet: 5,
pointsPerMeasurement: 10,
sharedTag: sharedTag,
sharedUser: sharedUser);
sharedUser: sharedUser,
sharedPreferences: sharedPreferences);
return new TestDataSet("Large (5x5x5x10)", order);
// Clear deeper level refs for realistic ~10% ratio
ClearDeepLevelRefs(order);
return new TestDataSet("Large (5x5x5x10)", order, iidRefPercent: 10);
}
private static TestDataSet CreateRepeatedStringsTestData()
{
TestDataFactory.ResetIdCounter();
// IId shared references
var sharedTag = TestDataFactory.CreateTag("RepeatedTag");
var sharedUser = TestDataFactory.CreateUser("repeateduser");
// Non-IId shared reference
var sharedPreferences = new UserPreferences
{
Theme = "dark",
Language = "en-US",
NotificationsEnabled = true,
EmailDigestFrequency = "weekly"
};
sharedUser.Preferences = sharedPreferences;
// Create order with many items to test string interning on repeated property names
var order = TestDataFactory.CreateOrder(
itemCount: 10,
palletsPerItem: 2,
measurementsPerPallet: 2,
pointsPerMeasurement: 2);
pointsPerMeasurement: 2,
sharedTag: sharedTag,
sharedUser: sharedUser,
sharedPreferences: sharedPreferences);
// Set same status and ProductName on all items to test enum and string handling
foreach (var item in order.Items)
@ -168,19 +230,79 @@ public static class Program
item.ProductName = "CommonProductName_RepeatedForTesting";
}
return new TestDataSet("Repeated Strings (10 items)", order);
// Clear deeper level refs for realistic ~10% ratio
ClearDeepLevelRefs(order);
return new TestDataSet("Repeated Strings (10 items)", order, iidRefPercent: 10);
}
/// <summary>
/// Clears IId shared references from Pallet, Measurement, and Point levels.
/// This creates a realistic ~10% IId ref ratio (only Order and Item levels have refs).
/// </summary>
private static void ClearDeepLevelRefs(TestOrder order)
{
foreach (var item in order.Items)
{
foreach (var pallet in item.Pallets)
{
pallet.Tag = null;
pallet.Inspector = null;
pallet.Category = null;
foreach (var measurement in pallet.Measurements)
{
measurement.Tag = null;
measurement.Operator = null;
foreach (var point in measurement.Points)
{
point.Tag = null;
point.Verifier = null;
}
}
}
}
}
private static TestDataSet CreateDeepNestedTestData()
{
TestDataFactory.ResetIdCounter();
// IId shared references - only at Order and Item levels for ~10% ratio
var sharedTag = TestDataFactory.CreateTag("DeepTag");
var sharedUser = TestDataFactory.CreateUser("deepuser");
var sharedCategory = TestDataFactory.CreateCategory("DeepCategory");
// Non-IId shared reference
var sharedPreferences = new UserPreferences
{
Theme = "light",
Language = "fr-FR",
NotificationsEnabled = false,
EmailDigestFrequency = "monthly"
};
sharedUser.Preferences = sharedPreferences;
var order = TestDataFactory.CreateOrder(
itemCount: 2,
palletsPerItem: 4,
measurementsPerPallet: 4,
pointsPerMeasurement: 8);
pointsPerMeasurement: 8,
sharedTag: sharedTag,
sharedUser: sharedUser,
sharedPreferences: sharedPreferences,
sharedCategory: sharedCategory);
return new TestDataSet("Deep Nested (2x4x4x8)", order);
// Clear deeper level refs for realistic ~10% ratio
ClearDeepLevelRefs(order);
return new TestDataSet("Deep Nested (2x4x4x8)", order, iidRefPercent: 10);
}
#endregion
@ -206,7 +328,7 @@ public static class Program
{
var result = new BenchmarkResult
{
TestDataName = testData.Name,
TestDataName = testData.DisplayName, // Use DisplayName for IId% info
SerializerName = serializer.Name,
SerializedSize = serializer.SerializedSize
};
@ -465,13 +587,29 @@ public static class Program
public string Name { get; }
public TestOrder Order { get; }
public TestDataSet(string name, TestOrder order)
/// <summary>
/// Percentage of IId shared references in the data (0-100).
/// Higher values mean more deduplication benefit for Default mode.
/// </summary>
public int IIdRefPercent { get; }
public TestDataSet(string name, TestOrder order, int iidRefPercent = 0)
{
Name = name;
Order = order;
IIdRefPercent = iidRefPercent;
}
/// <summary>
/// Gets display name including IId ref percentage if set.
/// </summary>
public string DisplayName => IIdRefPercent > 0
? $"{Name} [{IIdRefPercent}% IId refs]"
: Name;
}
private sealed class BenchmarkResult
{
public string TestDataName { get; set; } = "";
@ -498,11 +636,11 @@ public static class Program
foreach (var testData in testDataSets)
{
var testResults = results.Where(r => r.TestDataName == testData.Name).OrderBy(r => r.RoundTripTimeMs).ToList();
var testResults = results.Where(r => r.TestDataName == testData.DisplayName).OrderBy(r => r.RoundTripTimeMs).ToList();
var msgPackResult = testResults.FirstOrDefault(r => r.SerializerName == SerializerMessagePack);
var acBinaryResult = testResults.FirstOrDefault(r => r.SerializerName == SerializerAcBinaryDefault);
System.Console.WriteLine($"\n┌─ {testData.Name} ─".PadRight(98, '─') + "┐");
System.Console.WriteLine($"\n┌─ {testData.DisplayName} ─".PadRight(98, '─') + "┐");
System.Console.WriteLine($"│ {"#",-4} │ {"Serializer",-25} │ {"Size",-10} │ {"Serialize",-12} │ {"Deserialize",-12} │ {"Round-trip",-12} │");
System.Console.WriteLine($"├{"".PadRight(6, '─')}┼{"".PadRight(27, '─')}┼{"".PadRight(12, '─')}┼{"".PadRight(14, '─')}┼{"".PadRight(14, '─')}┼{"".PadRight(14, '─')}┤");

View File

@ -402,7 +402,7 @@ public class QuickBenchmark
// Warmup
Console.WriteLine("\nWarming up...");
for (int i = 0; i < 100; i++)
for (int i = 0; i < 10; i++)
{
_ = AcBinarySerializer.Serialize(testOrder, withRefOptions);
_ = AcBinarySerializer.Serialize(testOrder, noRefOptions);

View File

@ -218,7 +218,12 @@ public class TestPallet : IId<int>
// Level 4 collection
public List<TestMeasurement> Measurements { get; set; } = [];
// Shared references
// Shared IId references for better reference testing
public SharedTag? Tag { get; set; }
public SharedUser? Inspector { get; set; }
public SharedCategory? Category { get; set; }
// Non-IId shared references
public MetadataInfo? PalletMetadata { get; set; }
// Parent reference - ignored by all serializers to prevent circular references
@ -242,6 +247,10 @@ public class TestMeasurement : IId<int>
// Level 5 collection
public List<TestMeasurementPoint> Points { get; set; } = [];
// Shared IId references for better reference testing
public SharedTag? Tag { get; set; }
public SharedUser? Operator { get; set; }
// Parent reference - ignored by all serializers to prevent circular references
[JsonIgnore]
[IgnoreMember]
@ -260,6 +269,10 @@ public class TestMeasurementPoint : IId<int>
public double Value { get; set; }
public DateTime MeasuredAt { get; set; } = DateTime.UtcNow;
// Shared IId reference for better reference testing (many points share same tag/user)
public SharedTag? Tag { get; set; }
public SharedUser? Verifier { get; set; }
// Parent reference - ignored by all serializers to prevent circular references
[JsonIgnore]
[IgnoreMember]

View File

@ -104,7 +104,8 @@ public static class TestDataFactory
#region Hierarchy Creation (5 Levels)
/// <summary>
/// Create a deep order hierarchy with configurable depth
/// Create a deep order hierarchy with configurable depth.
/// Supports both IId-based (SharedTag, SharedUser, SharedCategory) and Non-IId (UserPreferences) shared references.
/// </summary>
public static TestOrder CreateOrder(
int itemCount = 2,
@ -113,8 +114,13 @@ public static class TestDataFactory
int pointsPerMeasurement = 3,
SharedTag? sharedTag = null,
SharedUser? sharedUser = null,
MetadataInfo? sharedMetadata = null)
MetadataInfo? sharedMetadata = null,
UserPreferences? sharedPreferences = null,
SharedCategory? sharedCategory = null)
{
// If sharedUser is provided but no sharedPreferences, use the user's preferences as shared
sharedPreferences ??= sharedUser?.Preferences;
var order = new TestOrder
{
Id = _idCounter++,
@ -125,6 +131,7 @@ public static class TestDataFactory
PrimaryTag = sharedTag,
SecondaryTag = sharedTag, // Same reference for $ref testing
Owner = sharedUser,
Category = sharedCategory,
OrderMetadata = sharedMetadata,
AuditMetadata = sharedMetadata // Same reference for Newtonsoft $ref
};
@ -136,7 +143,15 @@ public static class TestDataFactory
for (int i = 0; i < itemCount; i++)
{
var item = CreateOrderItem(palletsPerItem, measurementsPerPallet, pointsPerMeasurement, sharedTag, sharedUser, sharedMetadata);
var item = CreateOrderItem(
palletsPerItem,
measurementsPerPallet,
pointsPerMeasurement,
sharedTag,
sharedUser,
sharedMetadata,
sharedPreferences,
sharedCategory);
item.ParentOrder = order;
order.Items.Add(item);
}
@ -145,7 +160,8 @@ public static class TestDataFactory
}
/// <summary>
/// Create an order item with pallets
/// Create an order item with pallets.
/// Supports both IId-based and Non-IId shared references.
/// </summary>
public static TestOrderItem CreateOrderItem(
int palletCount = 2,
@ -153,8 +169,19 @@ public static class TestDataFactory
int pointsPerMeasurement = 3,
SharedTag? sharedTag = null,
SharedUser? sharedUser = null,
MetadataInfo? sharedMetadata = null)
MetadataInfo? sharedMetadata = null,
UserPreferences? sharedPreferences = null,
SharedCategory? sharedCategory = null)
{
// Create assignee - if sharedUser provided, use it. Otherwise create new user with sharedPreferences
SharedUser? assignee = sharedUser;
if (assignee == null && sharedPreferences != null)
{
// Create a new user but with shared preferences (Non-IId ref testing)
assignee = CreateUser();
assignee.Preferences = sharedPreferences;
}
var item = new TestOrderItem
{
Id = _idCounter++,
@ -163,13 +190,20 @@ public static class TestDataFactory
UnitPrice = 5.5m * _idCounter,
Status = TestStatus.Pending,
Tag = sharedTag,
Assignee = sharedUser,
Assignee = assignee,
ItemMetadata = sharedMetadata
};
for (int i = 0; i < palletCount; i++)
{
var pallet = CreatePallet(measurementsPerPallet, pointsPerMeasurement, sharedMetadata);
// Pass shared references to all levels - creates many shared refs!
var pallet = CreatePallet(
measurementsPerPallet,
pointsPerMeasurement,
sharedMetadata,
sharedTag, // IId shared ref
sharedUser, // IId shared ref
sharedCategory); // IId shared ref
pallet.ParentItem = item;
item.Pallets.Add(pallet);
}
@ -177,13 +211,19 @@ public static class TestDataFactory
return item;
}
/// <summary>
/// Create a pallet with measurements
/// </summary>
public static TestPallet CreatePallet(
int measurementCount = 2,
int pointsPerMeasurement = 3,
MetadataInfo? sharedMetadata = null)
MetadataInfo? sharedMetadata = null,
SharedTag? sharedTag = null,
SharedUser? sharedInspector = null,
SharedCategory? sharedCategory = null)
{
var pallet = new TestPallet
{
@ -192,12 +232,15 @@ public static class TestDataFactory
TrayCount = 5 + _idCounter % 10,
Status = TestStatus.Pending,
Weight = 100.5 + _idCounter,
PalletMetadata = sharedMetadata
PalletMetadata = sharedMetadata,
Tag = sharedTag,
Inspector = sharedInspector,
Category = sharedCategory
};
for (int i = 0; i < measurementCount; i++)
{
var measurement = CreateMeasurement(pointsPerMeasurement);
var measurement = CreateMeasurement(pointsPerMeasurement, sharedTag, sharedInspector);
measurement.ParentPallet = pallet;
pallet.Measurements.Add(measurement);
}
@ -208,19 +251,24 @@ public static class TestDataFactory
/// <summary>
/// Create a measurement with points
/// </summary>
public static TestMeasurement CreateMeasurement(int pointCount = 3)
public static TestMeasurement CreateMeasurement(
int pointCount = 3,
SharedTag? sharedTag = null,
SharedUser? sharedOperator = null)
{
var measurement = new TestMeasurement
{
Id = _idCounter++,
Name = $"Measurement-{_idCounter}",
TotalWeight = 100.5 + _idCounter,
CreatedAt = DateTime.UtcNow
CreatedAt = DateTime.UtcNow,
Tag = sharedTag,
Operator = sharedOperator
};
for (int i = 0; i < pointCount; i++)
{
var point = CreateMeasurementPoint();
var point = CreateMeasurementPoint(sharedTag, sharedOperator);
point.ParentMeasurement = measurement;
measurement.Points.Add(point);
}
@ -231,7 +279,9 @@ public static class TestDataFactory
/// <summary>
/// Create a measurement point
/// </summary>
public static TestMeasurementPoint CreateMeasurementPoint()
public static TestMeasurementPoint CreateMeasurementPoint(
SharedTag? sharedTag = null,
SharedUser? sharedVerifier = null)
{
var id = _idCounter++;
return new TestMeasurementPoint
@ -239,7 +289,9 @@ public static class TestDataFactory
Id = id,
Label = $"Point-{id}",
Value = 10.5 + (id * 0.1),
MeasuredAt = DateTime.UtcNow
MeasuredAt = DateTime.UtcNow,
Tag = sharedTag,
Verifier = sharedVerifier
};
}

View File

@ -934,9 +934,9 @@ public static partial class AcBinaryDeserializer
}
else
{
// Non-IId: [ObjectRef][hashcode] - lookup by hashcode
// Non-IId: [ObjectRef][hashcode] - lookup by hashcode (always Int32)
var hashcode = context.ReadVarInt();
if (context.ContextClass.TryGetValue(wrapper, hashcode, out var instance))
if (wrapper.TryGetValueInt32(hashcode, out var instance))
return instance;
throw new AcBinaryDeserializationException(
@ -949,7 +949,7 @@ public static partial class AcBinaryDeserializer
private static object? ReadObjectRefInt32(ref BinaryDeserializationContext context, ref TypeMetadataWrapper<BinaryDeserializeTypeMetadata> wrapper)
{
var id = context.ReadVarInt();
if (context.ContextClass.TryGetValue(wrapper, id, out var instance))
if (wrapper.TryGetValueInt32(id, out var instance))
return instance;
throw new AcBinaryDeserializationException(
@ -961,7 +961,7 @@ public static partial class AcBinaryDeserializer
private static object? ReadObjectRefInt64(ref BinaryDeserializationContext context, ref TypeMetadataWrapper<BinaryDeserializeTypeMetadata> wrapper)
{
var id = context.ReadVarLong();
if (context.ContextClass.TryGetValue(wrapper, id, out var instance))
if (wrapper.TryGetValueInt64(id, out var instance))
return instance;
throw new AcBinaryDeserializationException(
@ -973,7 +973,7 @@ public static partial class AcBinaryDeserializer
private static object? ReadObjectRefGuid(ref BinaryDeserializationContext context, ref TypeMetadataWrapper<BinaryDeserializeTypeMetadata> wrapper)
{
var id = context.ReadGuidUnsafe();
if (context.ContextClass.TryGetValue(wrapper, id, out var instance))
if (wrapper.TryGetValueGuid(id, out var instance))
return instance;
throw new AcBinaryDeserializationException(
@ -1012,22 +1012,8 @@ public static partial class AcBinaryDeserializer
PopulateObject(ref context, instance, metadata, depth, skipDefaultWrite: true);
// Register by Id after populate (Id is now set)
switch (metadata.IdAccessorType)
{
case AcSerializerCommon.IdAccessorType.Int32:
var intId = metadata.GetIdInt32(instance);
context.ContextClass.TryGetOrStoreInt32(wrapper, instance, intId);
break;
case AcSerializerCommon.IdAccessorType.Int64:
var longId = metadata.GetIdInt64(instance);
context.ContextClass.TryGetOrStoreLong(wrapper, instance, longId);
break;
case AcSerializerCommon.IdAccessorType.Guid:
var guidId = metadata.GetIdGuid(instance);
context.ContextClass.TryGetOrStoreGuid(wrapper, instance, guidId);
break;
}
// Register by Id after populate - uses typed methods on wrapper
RegisterIIdInstance(context.ContextClass, wrapper, instance, metadata);
}
else
{
@ -1035,13 +1021,13 @@ public static partial class AcBinaryDeserializer
var hashcode = context.ReadVarInt();
// TryGetValue - if already exists, return (shouldn't happen for Object, only ObjectRef)
if (context.ContextClass.TryGetValue(wrapper, hashcode, out instance))
if (wrapper.TryGetValueInt32(hashcode, out instance))
return instance;
// Create + Register by hashcode
// Create + Register by hashcode (always Int32 for Non-IId)
instance = CreateInstance(targetType, metadata);
if (instance == null) return null;
context.ContextClass.TryGetOrStoreInt32(wrapper, instance, hashcode);
wrapper.TryGetOrStoreInt32(hashcode, instance);
// Populate
PopulateObject(ref context, instance, metadata, depth, skipDefaultWrite: true);
@ -1060,23 +1046,7 @@ public static partial class AcBinaryDeserializer
// Note: For ChainMode, we need IdGetter OR typed getters (IdAccessorType != None)
if (context.IsChainMode && metadata.IsIId && metadata.IdType != null)
{
object? id;
switch (metadata.IdAccessorType)
{
case AcSerializerCommon.IdAccessorType.Int32:
id = metadata.GetIdInt32(instance);
break;
case AcSerializerCommon.IdAccessorType.Int64:
id = metadata.GetIdInt64(instance);
break;
case AcSerializerCommon.IdAccessorType.Guid:
id = metadata.GetIdGuid(instance);
break;
default:
id = null;
break;
}
var id = GetIdBoxed(instance, metadata);
if (id != null && !IsDefaultValue(id, metadata.IdType))
{
// Check if we already have this object
@ -1491,6 +1461,39 @@ public static partial class AcBinaryDeserializer
#endregion
#region IId Registration Helpers
/// <summary>
/// Registers an IId instance after populate - uses pre-bound delegate, NO SWITCH!
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void RegisterIIdInstance(
BinaryDeserializationContextClass contextClass,
TypeMetadataWrapper<BinaryDeserializeTypeMetadata> wrapper,
object instance,
BinaryDeserializeTypeMetadata metadata)
{
// Pre-bound delegate - no switch needed!
wrapper.RegisterById!(instance);
}
/// <summary>
/// Gets Id as boxed object - only used for ChainMode (rare path).
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static object? GetIdBoxed(object instance, BinaryDeserializeTypeMetadata metadata)
{
return metadata.IdAccessorType switch
{
AcSerializerCommon.IdAccessorType.Int32 => metadata.GetIdInt32(instance),
AcSerializerCommon.IdAccessorType.Int64 => metadata.GetIdInt64(instance),
AcSerializerCommon.IdAccessorType.Guid => metadata.GetIdGuid(instance),
_ => null
};
}
#endregion
#region Type Metadata
[MethodImpl(MethodImplOptions.AggressiveInlining)]

View File

@ -40,8 +40,8 @@ public abstract class SerializationContextBase<TMetadata, TOptions> : AcSerializ
{
Debug.Assert(wrapper.Metadata.IdAccessorType == AcSerializerCommon.IdAccessorType.Int32);
var getter = (Func<object, int>)wrapper.RefIdGetter;
refId = getter(obj);
// Use pre-cast getter - no cast overhead!
refId = wrapper.RefIdGetterInt32!(obj);
// BitArray fast path for small positive IDs
return refId is >= 0 and < MaxSmallId ? TryTrackSmallId(wrapper, refId) : wrapper.TryAddKey(refId);
@ -50,8 +50,8 @@ public abstract class SerializationContextBase<TMetadata, TOptions> : AcSerializ
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static bool TryTrackSmallId(TypeMetadataWrapper<TMetadata> wrapper, int id)
{
wrapper.EnsureSmallIdBitmap();
var bitmap = wrapper.SmallIdBitmap!;
// Lazy init - but only once per type per serialization
var bitmap = wrapper.SmallIdBitmap ?? InitSmallIdBitmap(wrapper);
var idx = (uint)id;
var wordIdx = idx >> 6;
@ -66,6 +66,13 @@ public abstract class SerializationContextBase<TMetadata, TOptions> : AcSerializ
return true;
}
[MethodImpl(MethodImplOptions.NoInlining)]
private static ulong[] InitSmallIdBitmap(TypeMetadataWrapper<TMetadata> wrapper)
{
wrapper.SmallIdBitmap = new ulong[BitArraySize];
return wrapper.SmallIdBitmap;
}
/// <summary>
/// Tries to track an object with long RefId.
/// Use when wrapper.Metadata.IdAccessorType == Int64.
@ -76,8 +83,8 @@ public abstract class SerializationContextBase<TMetadata, TOptions> : AcSerializ
{
Debug.Assert(wrapper.Metadata.IdAccessorType == AcSerializerCommon.IdAccessorType.Int64);
var getter = (Func<object, long>)wrapper.RefIdGetter;
refId = getter(obj);
// Use pre-cast getter - no cast overhead!
refId = wrapper.RefIdGetterInt64!(obj);
return wrapper.TryAddKey(refId);
}
@ -92,8 +99,8 @@ public abstract class SerializationContextBase<TMetadata, TOptions> : AcSerializ
{
Debug.Assert(wrapper.Metadata.IdAccessorType == AcSerializerCommon.IdAccessorType.Guid);
var getter = (Func<object, Guid>)wrapper.RefIdGetter;
refId = getter(obj);
// Use pre-cast getter - no cast overhead!
refId = wrapper.RefIdGetterGuid!(obj);
return wrapper.TryAddKey(refId);
}

View File

@ -8,7 +8,7 @@ namespace AyCode.Core.Serializers;
/// <summary>
/// Wrapper that combines metadata with tracking state.
/// Each context has one wrapper per type - contains all type-specific info and state.
/// Not generic on TRefId - uses runtime typed Delegate and object for flexibility.
/// Uses typed fields instead of generics for zero-overhead Id operations.
/// </summary>
/// <typeparam name="TMetadata">The concrete metadata type (BinarySerializeTypeMetadata, etc.)</typeparam>
public sealed class TypeMetadataWrapper<TMetadata> where TMetadata : TypeMetadataBase
@ -19,20 +19,45 @@ public sealed class TypeMetadataWrapper<TMetadata> where TMetadata : TypeMetadat
public readonly TMetadata Metadata;
/// <summary>
/// Typed getter for reference ID. Runtime type is Func&lt;object, int/long/Guid&gt;.
/// Use IdAccessorType to determine the actual type.
/// For IId types: uses metadata.TypedIdGetter (cached in metadata).
/// For non-IId types: uses RuntimeHelpers.GetHashCode.
/// Pre-cast Int32 getter for fast path (no cast needed in TryTrack).
/// Set when IdAccessorType == Int32.
/// </summary>
internal readonly Delegate RefIdGetter;
internal readonly Func<object, int>? RefIdGetterInt32;
/// <summary>
/// Identity map for tracking. Runtime type is IdentityMap&lt;int/long/Guid&gt;.
/// Pre-cast Int64 getter for fast path.
/// Set when IdAccessorType == Int64.
/// </summary>
protected AcSerializerCommon.IIdentityMap? IdentityMap;
internal readonly Func<object, long>? RefIdGetterInt64;
/// <summary>
/// BitArray for tracking small int32 IDs (0-65535).
/// Pre-cast Guid getter for fast path.
/// Set when IdAccessorType == Guid.
/// </summary>
internal readonly Func<object, Guid>? RefIdGetterGuid;
#region Typed IdentityMaps - No generic type checks in hot path!
/// <summary>
/// Typed IdentityMap for Int32 IDs. Direct access, no type check.
/// </summary>
internal AcSerializerCommon.IdentityMap<int>? IdentityMapInt32;
/// <summary>
/// Typed IdentityMap for Int64 IDs. Direct access, no type check.
/// </summary>
internal AcSerializerCommon.IdentityMap<long>? IdentityMapInt64;
/// <summary>
/// Typed IdentityMap for Guid IDs. Direct access, no type check.
/// </summary>
internal AcSerializerCommon.IdentityMap<Guid>? IdentityMapGuid;
#endregion
/// <summary>
/// BitArray for tracking small int32 IDs (0-65535) during serialization.
/// Only used when IdAccessorType == Int32.
/// </summary>
internal ulong[]? SmallIdBitmap;
@ -45,16 +70,40 @@ public sealed class TypeMetadataWrapper<TMetadata> where TMetadata : TypeMetadat
/// </summary>
private static readonly Func<object, int> HashCodeGetter = RuntimeHelpers.GetHashCode;
/// <summary>
/// Pre-bound delegate to register instance by Id - NO SWITCH in hot path!
/// Set in constructor based on IdAccessorType. Method group conversion = no allocation.
/// </summary>
internal readonly Action<object>? RegisterById;
/// <summary>
/// Creates a new wrapper for the given metadata.
/// Uses metadata.TypedIdGetter for IId types (cached, no allocation).
/// Pre-casts typed getters for zero-overhead access in hot path.
/// </summary>
public TypeMetadataWrapper(TMetadata metadata)
{
Metadata = metadata;
// Use cached delegate from metadata for IId types, static fallback for non-IId
RefIdGetter = metadata.TypedIdGetter ?? HashCodeGetter;
var refIdGetter = metadata.TypedIdGetter ?? HashCodeGetter;
// Pre-cast typed getters AND set RegisterById delegate - avoids switch in every call
switch (metadata.IdAccessorType)
{
case AcSerializerCommon.IdAccessorType.Int32:
RefIdGetterInt32 = (Func<object, int>)refIdGetter;
RegisterById = RegisterByInt32Id; // Method group - no allocation!
break;
case AcSerializerCommon.IdAccessorType.Int64:
RefIdGetterInt64 = (Func<object, long>)refIdGetter;
RegisterById = RegisterByInt64Id;
break;
case AcSerializerCommon.IdAccessorType.Guid:
RefIdGetterGuid = (Func<object, Guid>)refIdGetter;
RegisterById = RegisterByGuidId;
break;
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
@ -65,7 +114,7 @@ public sealed class TypeMetadataWrapper<TMetadata> where TMetadata : TypeMetadat
/// <summary>
/// Resets tracking state for reuse between serializations.
/// Does not deallocate - just clears for reuse.
/// Does not deallocate - just clears for reuse (pool-friendly).
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void ResetTracking()
@ -73,31 +122,149 @@ public sealed class TypeMetadataWrapper<TMetadata> where TMetadata : TypeMetadat
if (SmallIdBitmap != null)
Array.Clear(SmallIdBitmap);
IdentityMap?.Reset();
IdentityMapInt32?.Reset();
IdentityMapInt64?.Reset();
IdentityMapGuid?.Reset();
}
#region Direct Int32 Operations - No type check, no generic overhead
/// <summary>
/// Tries to get object by Int32 Id. Returns true if found.
/// Direct dictionary lookup - no generic type check!
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool TryGetValueInt32(int id, out object? instance)
{
if (IdentityMapInt32 != null)
return IdentityMapInt32.TryGetValue(id, out instance);
instance = null;
return false;
}
/// <summary>
/// Ensures SmallIdBitmap is allocated (lazy allocation).
/// Gets existing object or stores new one by Int32 Id.
/// Direct dictionary access - no generic type check!
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal void EnsureSmallIdBitmap()
public object TryGetOrStoreInt32(int id, object newObj)
{
SmallIdBitmap ??= new ulong[BitArraySize];
if (id == 0) return newObj; // Default Id - no tracking
var map = IdentityMapInt32 ??= new AcSerializerCommon.IdentityMap<int>();
return map.TryGetOrAddValue(id, newObj);
}
/// <summary>
/// Registers IId instance by extracting its Int32 Id.
/// Combines Id getter + store in one call.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void RegisterByInt32Id(object instance)
{
var id = RefIdGetterInt32!(instance);
if (id == 0) return;
var map = IdentityMapInt32 ??= new AcSerializerCommon.IdentityMap<int>();
map.TryGetOrAddValue(id, instance);
}
#endregion
#region Direct Int64 Operations
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool TryGetValueInt64(long id, out object? instance)
{
if (IdentityMapInt64 != null)
return IdentityMapInt64.TryGetValue(id, out instance);
instance = null;
return false;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public object TryGetOrStoreInt64(long id, object newObj)
{
if (id == 0) return newObj;
var map = IdentityMapInt64 ??= new AcSerializerCommon.IdentityMap<long>();
return map.TryGetOrAddValue(id, newObj);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void RegisterByInt64Id(object instance)
{
var id = RefIdGetterInt64!(instance);
if (id == 0) return;
var map = IdentityMapInt64 ??= new AcSerializerCommon.IdentityMap<long>();
map.TryGetOrAddValue(id, instance);
}
#endregion
#region Direct Guid Operations
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool TryGetValueGuid(Guid id, out object? instance)
{
if (IdentityMapGuid != null)
return IdentityMapGuid.TryGetValue(id, out instance);
instance = null;
return false;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public object TryGetOrStoreGuid(Guid id, object newObj)
{
if (id == Guid.Empty) return newObj;
var map = IdentityMapGuid ??= new AcSerializerCommon.IdentityMap<Guid>();
return map.TryGetOrAddValue(id, newObj);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void RegisterByGuidId(object instance)
{
var id = RefIdGetterGuid!(instance);
if (id == Guid.Empty) return;
var map = IdentityMapGuid ??= new AcSerializerCommon.IdentityMap<Guid>();
map.TryGetOrAddValue(id, instance);
}
#endregion
#region Legacy Generic Methods (for backward compatibility)
/// <summary>
/// Gets or creates the typed IdentityMap for tracking.
/// Use: wrapper.GetOrCreateIdentityMap&lt;int&gt;().TryAddKey(id)
/// SLOW PATH - use typed methods (TryGetValueInt32, etc.) instead!
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public AcSerializerCommon.IdentityMap<TId> GetOrCreateIdentityMap<TId>() where TId : notnull
{
if (IdentityMap is AcSerializerCommon.IdentityMap<TId> typedMap)
return typedMap;
// Route to typed fields based on TId
if (typeof(TId) == typeof(int))
{
var map = IdentityMapInt32 ??= new AcSerializerCommon.IdentityMap<int>();
return Unsafe.As<AcSerializerCommon.IdentityMap<int>, AcSerializerCommon.IdentityMap<TId>>(ref map);
}
if (typeof(TId) == typeof(long))
{
var map = IdentityMapInt64 ??= new AcSerializerCommon.IdentityMap<long>();
return Unsafe.As<AcSerializerCommon.IdentityMap<long>, AcSerializerCommon.IdentityMap<TId>>(ref map);
}
if (typeof(TId) == typeof(Guid))
{
var map = IdentityMapGuid ??= new AcSerializerCommon.IdentityMap<Guid>();
return Unsafe.As<AcSerializerCommon.IdentityMap<Guid>, AcSerializerCommon.IdentityMap<TId>>(ref map);
}
var newMap = new AcSerializerCommon.IdentityMap<TId>();
IdentityMap = newMap;
return newMap;
throw new NotSupportedException($"Id type {typeof(TId)} is not supported");
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
@ -117,9 +284,9 @@ public sealed class TypeMetadataWrapper<TMetadata> where TMetadata : TypeMetadat
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public object TryGetOrStoreId<TId>(TId id, object newObj) where TId : struct
{
//if (id == default(TId)) return newObj; // Default Id - no tracking
var map = GetOrCreateIdentityMap<TId>();
return map.TryGetOrAddValue(id, newObj);
}
#endregion
}