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) foreach (var testData in testDataSets)
{ {
System.Console.WriteLine($"\n{'═'.ToString().PadRight(70, '═')}"); 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, '═')}"); System.Console.WriteLine($"{'═'.ToString().PadRight(70, '═')}");
var results = RunBenchmarksForTestData(testData, mode); var results = RunBenchmarksForTestData(testData, mode);
@ -83,6 +83,8 @@ public static class Program
// Print grouped results // Print grouped results
PrintGroupedResults(allResults, testDataSets); PrintGroupedResults(allResults, testDataSets);
// Save results to file // Save results to file
SaveResults(allResults, testDataSets); SaveResults(allResults, testDataSets);
@ -106,22 +108,44 @@ public static class Program
private static TestDataSet CreateSmallTestData() private static TestDataSet CreateSmallTestData()
{ {
TestDataFactory.ResetIdCounter(); 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( var order = TestDataFactory.CreateOrder(
itemCount: 2, itemCount: 2,
palletsPerItem: 2, palletsPerItem: 2,
measurementsPerPallet: 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() private static TestDataSet CreateMediumTestData()
{ {
TestDataFactory.ResetIdCounter(); TestDataFactory.ResetIdCounter();
// IId shared references
var sharedTag = TestDataFactory.CreateTag("SharedTag"); var sharedTag = TestDataFactory.CreateTag("SharedTag");
var sharedUser = TestDataFactory.CreateUser("shareduser"); var sharedUser = TestDataFactory.CreateUser("shareduser");
var sharedMeta = TestDataFactory.CreateMetadata("shared", withChild: true); 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( var order = TestDataFactory.CreateOrder(
itemCount: 3, itemCount: 3,
palletsPerItem: 3, palletsPerItem: 3,
@ -129,37 +153,75 @@ public static class Program
pointsPerMeasurement: 4, pointsPerMeasurement: 4,
sharedTag: sharedTag, sharedTag: sharedTag,
sharedUser: sharedUser, 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() private static TestDataSet CreateLargeTestData()
{ {
TestDataFactory.ResetIdCounter(); TestDataFactory.ResetIdCounter();
// IId shared references
var sharedTag = TestDataFactory.CreateTag("SharedTag"); var sharedTag = TestDataFactory.CreateTag("SharedTag");
var sharedUser = TestDataFactory.CreateUser("shareduser"); 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( var order = TestDataFactory.CreateOrder(
itemCount: 5, itemCount: 5,
palletsPerItem: 5, palletsPerItem: 5,
measurementsPerPallet: 5, measurementsPerPallet: 5,
pointsPerMeasurement: 10, pointsPerMeasurement: 10,
sharedTag: sharedTag, 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() private static TestDataSet CreateRepeatedStringsTestData()
{ {
TestDataFactory.ResetIdCounter(); 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 // Create order with many items to test string interning on repeated property names
var order = TestDataFactory.CreateOrder( var order = TestDataFactory.CreateOrder(
itemCount: 10, itemCount: 10,
palletsPerItem: 2, palletsPerItem: 2,
measurementsPerPallet: 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 // Set same status and ProductName on all items to test enum and string handling
foreach (var item in order.Items) foreach (var item in order.Items)
@ -168,19 +230,79 @@ public static class Program
item.ProductName = "CommonProductName_RepeatedForTesting"; 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() private static TestDataSet CreateDeepNestedTestData()
{ {
TestDataFactory.ResetIdCounter(); 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( var order = TestDataFactory.CreateOrder(
itemCount: 2, itemCount: 2,
palletsPerItem: 4, palletsPerItem: 4,
measurementsPerPallet: 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 #endregion
@ -206,7 +328,7 @@ public static class Program
{ {
var result = new BenchmarkResult var result = new BenchmarkResult
{ {
TestDataName = testData.Name, TestDataName = testData.DisplayName, // Use DisplayName for IId% info
SerializerName = serializer.Name, SerializerName = serializer.Name,
SerializedSize = serializer.SerializedSize SerializedSize = serializer.SerializedSize
}; };
@ -465,13 +587,29 @@ public static class Program
public string Name { get; } public string Name { get; }
public TestOrder Order { 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; Name = name;
Order = order; 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 private sealed class BenchmarkResult
{ {
public string TestDataName { get; set; } = ""; public string TestDataName { get; set; } = "";
@ -498,11 +636,11 @@ public static class Program
foreach (var testData in testDataSets) 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 msgPackResult = testResults.FirstOrDefault(r => r.SerializerName == SerializerMessagePack);
var acBinaryResult = testResults.FirstOrDefault(r => r.SerializerName == SerializerAcBinaryDefault); 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($"│ {"#",-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, '─')}┤"); 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 // Warmup
Console.WriteLine("\nWarming up..."); 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, withRefOptions);
_ = AcBinarySerializer.Serialize(testOrder, noRefOptions); _ = AcBinarySerializer.Serialize(testOrder, noRefOptions);

View File

@ -218,7 +218,12 @@ public class TestPallet : IId<int>
// Level 4 collection // Level 4 collection
public List<TestMeasurement> Measurements { get; set; } = []; 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; } public MetadataInfo? PalletMetadata { get; set; }
// Parent reference - ignored by all serializers to prevent circular references // Parent reference - ignored by all serializers to prevent circular references
@ -242,6 +247,10 @@ public class TestMeasurement : IId<int>
// Level 5 collection // Level 5 collection
public List<TestMeasurementPoint> Points { get; set; } = []; 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 // Parent reference - ignored by all serializers to prevent circular references
[JsonIgnore] [JsonIgnore]
[IgnoreMember] [IgnoreMember]
@ -260,6 +269,10 @@ public class TestMeasurementPoint : IId<int>
public double Value { get; set; } public double Value { get; set; }
public DateTime MeasuredAt { get; set; } = DateTime.UtcNow; 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 // Parent reference - ignored by all serializers to prevent circular references
[JsonIgnore] [JsonIgnore]
[IgnoreMember] [IgnoreMember]

View File

@ -104,7 +104,8 @@ public static class TestDataFactory
#region Hierarchy Creation (5 Levels) #region Hierarchy Creation (5 Levels)
/// <summary> /// <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> /// </summary>
public static TestOrder CreateOrder( public static TestOrder CreateOrder(
int itemCount = 2, int itemCount = 2,
@ -113,8 +114,13 @@ public static class TestDataFactory
int pointsPerMeasurement = 3, int pointsPerMeasurement = 3,
SharedTag? sharedTag = null, SharedTag? sharedTag = null,
SharedUser? sharedUser = 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 var order = new TestOrder
{ {
Id = _idCounter++, Id = _idCounter++,
@ -125,6 +131,7 @@ public static class TestDataFactory
PrimaryTag = sharedTag, PrimaryTag = sharedTag,
SecondaryTag = sharedTag, // Same reference for $ref testing SecondaryTag = sharedTag, // Same reference for $ref testing
Owner = sharedUser, Owner = sharedUser,
Category = sharedCategory,
OrderMetadata = sharedMetadata, OrderMetadata = sharedMetadata,
AuditMetadata = sharedMetadata // Same reference for Newtonsoft $ref AuditMetadata = sharedMetadata // Same reference for Newtonsoft $ref
}; };
@ -136,7 +143,15 @@ public static class TestDataFactory
for (int i = 0; i < itemCount; i++) 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; item.ParentOrder = order;
order.Items.Add(item); order.Items.Add(item);
} }
@ -145,7 +160,8 @@ public static class TestDataFactory
} }
/// <summary> /// <summary>
/// Create an order item with pallets /// Create an order item with pallets.
/// Supports both IId-based and Non-IId shared references.
/// </summary> /// </summary>
public static TestOrderItem CreateOrderItem( public static TestOrderItem CreateOrderItem(
int palletCount = 2, int palletCount = 2,
@ -153,8 +169,19 @@ public static class TestDataFactory
int pointsPerMeasurement = 3, int pointsPerMeasurement = 3,
SharedTag? sharedTag = null, SharedTag? sharedTag = null,
SharedUser? sharedUser = 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 var item = new TestOrderItem
{ {
Id = _idCounter++, Id = _idCounter++,
@ -163,13 +190,20 @@ public static class TestDataFactory
UnitPrice = 5.5m * _idCounter, UnitPrice = 5.5m * _idCounter,
Status = TestStatus.Pending, Status = TestStatus.Pending,
Tag = sharedTag, Tag = sharedTag,
Assignee = sharedUser, Assignee = assignee,
ItemMetadata = sharedMetadata ItemMetadata = sharedMetadata
}; };
for (int i = 0; i < palletCount; i++) 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; pallet.ParentItem = item;
item.Pallets.Add(pallet); item.Pallets.Add(pallet);
} }
@ -177,13 +211,19 @@ public static class TestDataFactory
return item; return item;
} }
/// <summary> /// <summary>
/// Create a pallet with measurements /// Create a pallet with measurements
/// </summary> /// </summary>
public static TestPallet CreatePallet( public static TestPallet CreatePallet(
int measurementCount = 2, int measurementCount = 2,
int pointsPerMeasurement = 3, int pointsPerMeasurement = 3,
MetadataInfo? sharedMetadata = null) MetadataInfo? sharedMetadata = null,
SharedTag? sharedTag = null,
SharedUser? sharedInspector = null,
SharedCategory? sharedCategory = null)
{ {
var pallet = new TestPallet var pallet = new TestPallet
{ {
@ -192,12 +232,15 @@ public static class TestDataFactory
TrayCount = 5 + _idCounter % 10, TrayCount = 5 + _idCounter % 10,
Status = TestStatus.Pending, Status = TestStatus.Pending,
Weight = 100.5 + _idCounter, Weight = 100.5 + _idCounter,
PalletMetadata = sharedMetadata PalletMetadata = sharedMetadata,
Tag = sharedTag,
Inspector = sharedInspector,
Category = sharedCategory
}; };
for (int i = 0; i < measurementCount; i++) for (int i = 0; i < measurementCount; i++)
{ {
var measurement = CreateMeasurement(pointsPerMeasurement); var measurement = CreateMeasurement(pointsPerMeasurement, sharedTag, sharedInspector);
measurement.ParentPallet = pallet; measurement.ParentPallet = pallet;
pallet.Measurements.Add(measurement); pallet.Measurements.Add(measurement);
} }
@ -208,19 +251,24 @@ public static class TestDataFactory
/// <summary> /// <summary>
/// Create a measurement with points /// Create a measurement with points
/// </summary> /// </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 var measurement = new TestMeasurement
{ {
Id = _idCounter++, Id = _idCounter++,
Name = $"Measurement-{_idCounter}", Name = $"Measurement-{_idCounter}",
TotalWeight = 100.5 + _idCounter, TotalWeight = 100.5 + _idCounter,
CreatedAt = DateTime.UtcNow CreatedAt = DateTime.UtcNow,
Tag = sharedTag,
Operator = sharedOperator
}; };
for (int i = 0; i < pointCount; i++) for (int i = 0; i < pointCount; i++)
{ {
var point = CreateMeasurementPoint(); var point = CreateMeasurementPoint(sharedTag, sharedOperator);
point.ParentMeasurement = measurement; point.ParentMeasurement = measurement;
measurement.Points.Add(point); measurement.Points.Add(point);
} }
@ -231,7 +279,9 @@ public static class TestDataFactory
/// <summary> /// <summary>
/// Create a measurement point /// Create a measurement point
/// </summary> /// </summary>
public static TestMeasurementPoint CreateMeasurementPoint() public static TestMeasurementPoint CreateMeasurementPoint(
SharedTag? sharedTag = null,
SharedUser? sharedVerifier = null)
{ {
var id = _idCounter++; var id = _idCounter++;
return new TestMeasurementPoint return new TestMeasurementPoint
@ -239,7 +289,9 @@ public static class TestDataFactory
Id = id, Id = id,
Label = $"Point-{id}", Label = $"Point-{id}",
Value = 10.5 + (id * 0.1), 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 else
{ {
// Non-IId: [ObjectRef][hashcode] - lookup by hashcode // Non-IId: [ObjectRef][hashcode] - lookup by hashcode (always Int32)
var hashcode = context.ReadVarInt(); var hashcode = context.ReadVarInt();
if (context.ContextClass.TryGetValue(wrapper, hashcode, out var instance)) if (wrapper.TryGetValueInt32(hashcode, out var instance))
return instance; return instance;
throw new AcBinaryDeserializationException( throw new AcBinaryDeserializationException(
@ -949,7 +949,7 @@ public static partial class AcBinaryDeserializer
private static object? ReadObjectRefInt32(ref BinaryDeserializationContext context, ref TypeMetadataWrapper<BinaryDeserializeTypeMetadata> wrapper) private static object? ReadObjectRefInt32(ref BinaryDeserializationContext context, ref TypeMetadataWrapper<BinaryDeserializeTypeMetadata> wrapper)
{ {
var id = context.ReadVarInt(); var id = context.ReadVarInt();
if (context.ContextClass.TryGetValue(wrapper, id, out var instance)) if (wrapper.TryGetValueInt32(id, out var instance))
return instance; return instance;
throw new AcBinaryDeserializationException( throw new AcBinaryDeserializationException(
@ -961,7 +961,7 @@ public static partial class AcBinaryDeserializer
private static object? ReadObjectRefInt64(ref BinaryDeserializationContext context, ref TypeMetadataWrapper<BinaryDeserializeTypeMetadata> wrapper) private static object? ReadObjectRefInt64(ref BinaryDeserializationContext context, ref TypeMetadataWrapper<BinaryDeserializeTypeMetadata> wrapper)
{ {
var id = context.ReadVarLong(); var id = context.ReadVarLong();
if (context.ContextClass.TryGetValue(wrapper, id, out var instance)) if (wrapper.TryGetValueInt64(id, out var instance))
return instance; return instance;
throw new AcBinaryDeserializationException( throw new AcBinaryDeserializationException(
@ -973,7 +973,7 @@ public static partial class AcBinaryDeserializer
private static object? ReadObjectRefGuid(ref BinaryDeserializationContext context, ref TypeMetadataWrapper<BinaryDeserializeTypeMetadata> wrapper) private static object? ReadObjectRefGuid(ref BinaryDeserializationContext context, ref TypeMetadataWrapper<BinaryDeserializeTypeMetadata> wrapper)
{ {
var id = context.ReadGuidUnsafe(); var id = context.ReadGuidUnsafe();
if (context.ContextClass.TryGetValue(wrapper, id, out var instance)) if (wrapper.TryGetValueGuid(id, out var instance))
return instance; return instance;
throw new AcBinaryDeserializationException( throw new AcBinaryDeserializationException(
@ -1012,22 +1012,8 @@ public static partial class AcBinaryDeserializer
PopulateObject(ref context, instance, metadata, depth, skipDefaultWrite: true); PopulateObject(ref context, instance, metadata, depth, skipDefaultWrite: true);
// Register by Id after populate (Id is now set) // Register by Id after populate - uses typed methods on wrapper
switch (metadata.IdAccessorType) RegisterIIdInstance(context.ContextClass, wrapper, instance, metadata);
{
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;
}
} }
else else
{ {
@ -1035,13 +1021,13 @@ public static partial class AcBinaryDeserializer
var hashcode = context.ReadVarInt(); var hashcode = context.ReadVarInt();
// TryGetValue - if already exists, return (shouldn't happen for Object, only ObjectRef) // 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; return instance;
// Create + Register by hashcode // Create + Register by hashcode (always Int32 for Non-IId)
instance = CreateInstance(targetType, metadata); instance = CreateInstance(targetType, metadata);
if (instance == null) return null; if (instance == null) return null;
context.ContextClass.TryGetOrStoreInt32(wrapper, instance, hashcode); wrapper.TryGetOrStoreInt32(hashcode, instance);
// Populate // Populate
PopulateObject(ref context, instance, metadata, depth, skipDefaultWrite: true); 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) // Note: For ChainMode, we need IdGetter OR typed getters (IdAccessorType != None)
if (context.IsChainMode && metadata.IsIId && metadata.IdType != null) if (context.IsChainMode && metadata.IsIId && metadata.IdType != null)
{ {
object? id; var id = GetIdBoxed(instance, metadata);
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;
}
if (id != null && !IsDefaultValue(id, metadata.IdType)) if (id != null && !IsDefaultValue(id, metadata.IdType))
{ {
// Check if we already have this object // Check if we already have this object
@ -1491,6 +1461,39 @@ public static partial class AcBinaryDeserializer
#endregion #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 #region Type Metadata
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]

View File

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

View File

@ -8,7 +8,7 @@ namespace AyCode.Core.Serializers;
/// <summary> /// <summary>
/// Wrapper that combines metadata with tracking state. /// Wrapper that combines metadata with tracking state.
/// Each context has one wrapper per type - contains all type-specific info and 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> /// </summary>
/// <typeparam name="TMetadata">The concrete metadata type (BinarySerializeTypeMetadata, etc.)</typeparam> /// <typeparam name="TMetadata">The concrete metadata type (BinarySerializeTypeMetadata, etc.)</typeparam>
public sealed class TypeMetadataWrapper<TMetadata> where TMetadata : TypeMetadataBase public sealed class TypeMetadataWrapper<TMetadata> where TMetadata : TypeMetadataBase
@ -19,20 +19,45 @@ public sealed class TypeMetadataWrapper<TMetadata> where TMetadata : TypeMetadat
public readonly TMetadata Metadata; public readonly TMetadata Metadata;
/// <summary> /// <summary>
/// Typed getter for reference ID. Runtime type is Func&lt;object, int/long/Guid&gt;. /// Pre-cast Int32 getter for fast path (no cast needed in TryTrack).
/// Use IdAccessorType to determine the actual type. /// Set when IdAccessorType == Int32.
/// For IId types: uses metadata.TypedIdGetter (cached in metadata).
/// For non-IId types: uses RuntimeHelpers.GetHashCode.
/// </summary> /// </summary>
internal readonly Delegate RefIdGetter; internal readonly Func<object, int>? RefIdGetterInt32;
/// <summary> /// <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> /// </summary>
protected AcSerializerCommon.IIdentityMap? IdentityMap; internal readonly Func<object, long>? RefIdGetterInt64;
/// <summary> /// <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. /// Only used when IdAccessorType == Int32.
/// </summary> /// </summary>
internal ulong[]? SmallIdBitmap; internal ulong[]? SmallIdBitmap;
@ -45,16 +70,40 @@ public sealed class TypeMetadataWrapper<TMetadata> where TMetadata : TypeMetadat
/// </summary> /// </summary>
private static readonly Func<object, int> HashCodeGetter = RuntimeHelpers.GetHashCode; 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> /// <summary>
/// Creates a new wrapper for the given metadata. /// Creates a new wrapper for the given metadata.
/// Uses metadata.TypedIdGetter for IId types (cached, no allocation). /// Uses metadata.TypedIdGetter for IId types (cached, no allocation).
/// Pre-casts typed getters for zero-overhead access in hot path.
/// </summary> /// </summary>
public TypeMetadataWrapper(TMetadata metadata) public TypeMetadataWrapper(TMetadata metadata)
{ {
Metadata = metadata; Metadata = metadata;
// Use cached delegate from metadata for IId types, static fallback for non-IId // 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)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
@ -65,7 +114,7 @@ public sealed class TypeMetadataWrapper<TMetadata> where TMetadata : TypeMetadat
/// <summary> /// <summary>
/// Resets tracking state for reuse between serializations. /// Resets tracking state for reuse between serializations.
/// Does not deallocate - just clears for reuse. /// Does not deallocate - just clears for reuse (pool-friendly).
/// </summary> /// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public void ResetTracking() public void ResetTracking()
@ -73,31 +122,149 @@ public sealed class TypeMetadataWrapper<TMetadata> where TMetadata : TypeMetadat
if (SmallIdBitmap != null) if (SmallIdBitmap != null)
Array.Clear(SmallIdBitmap); 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> /// <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> /// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)] [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> /// <summary>
/// Gets or creates the typed IdentityMap for tracking. /// 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> /// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public AcSerializerCommon.IdentityMap<TId> GetOrCreateIdentityMap<TId>() where TId : notnull public AcSerializerCommon.IdentityMap<TId> GetOrCreateIdentityMap<TId>() where TId : notnull
{ {
if (IdentityMap is AcSerializerCommon.IdentityMap<TId> typedMap) // Route to typed fields based on TId
return typedMap; 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>(); throw new NotSupportedException($"Id type {typeof(TId)} is not supported");
IdentityMap = newMap;
return newMap;
} }
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
@ -117,9 +284,9 @@ public sealed class TypeMetadataWrapper<TMetadata> where TMetadata : TypeMetadat
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public object TryGetOrStoreId<TId>(TId id, object newObj) where TId : struct 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>(); var map = GetOrCreateIdentityMap<TId>();
return map.TryGetOrAddValue(id, newObj); return map.TryGetOrAddValue(id, newObj);
} }
#endregion
} }