From 6df5c53937edfdfbf3a0f69be7f05881baf92eef Mon Sep 17 00:00:00 2001 From: Loretta Date: Sat, 24 Jan 2026 01:39:30 +0100 Subject: [PATCH] Improve shared reference handling & benchmark realism MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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. --- AyCode.Core.Serializers.Console/Program.cs | 168 ++++++++++++-- .../Serialization/QuickBenchmark.cs | 2 +- .../TestModels/SharedTestModels.cs | 15 +- .../TestModels/TestDataFactory.cs | 82 +++++-- .../Binaries/AcBinaryDeserializer.cs | 85 +++---- .../Serializers/SerializationContextBase.cs | 23 +- .../Serializers/TypeMetadataWrapper.cs | 219 +++++++++++++++--- 7 files changed, 487 insertions(+), 107 deletions(-) diff --git a/AyCode.Core.Serializers.Console/Program.cs b/AyCode.Core.Serializers.Console/Program.cs index a7b1e34..5a600a6 100644 --- a/AyCode.Core.Serializers.Console/Program.cs +++ b/AyCode.Core.Serializers.Console/Program.cs @@ -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,21 +108,43 @@ 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, @@ -129,16 +153,32 @@ 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, @@ -146,20 +186,42 @@ public static class Program 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) @@ -167,20 +229,80 @@ public static class Program item.Status = TestStatus.Processing; item.ProductName = "CommonProductName_RepeatedForTesting"; } + + // Clear deeper level refs for realistic ~10% ratio + ClearDeepLevelRefs(order); - return new TestDataSet("Repeated Strings (10 items)", order); + return new TestDataSet("Repeated Strings (10 items)", order, iidRefPercent: 10); } + + /// + /// 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). + /// + 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); + + // Clear deeper level refs for realistic ~10% ratio + ClearDeepLevelRefs(order); - return new TestDataSet("Deep Nested (2x4x4x8)", 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 }; @@ -464,14 +586,30 @@ public static class Program { public string Name { get; } public TestOrder Order { get; } + + /// + /// Percentage of IId shared references in the data (0-100). + /// Higher values mean more deduplication benefit for Default mode. + /// + public int IIdRefPercent { get; } - public TestDataSet(string name, TestOrder order) + public TestDataSet(string name, TestOrder order, int iidRefPercent = 0) { Name = name; Order = order; + IIdRefPercent = iidRefPercent; } + + /// + /// Gets display name including IId ref percentage if set. + /// + 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, '─')}┤"); diff --git a/AyCode.Core.Tests/Serialization/QuickBenchmark.cs b/AyCode.Core.Tests/Serialization/QuickBenchmark.cs index 27031a5..b8054bb 100644 --- a/AyCode.Core.Tests/Serialization/QuickBenchmark.cs +++ b/AyCode.Core.Tests/Serialization/QuickBenchmark.cs @@ -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); diff --git a/AyCode.Core.Tests/TestModels/SharedTestModels.cs b/AyCode.Core.Tests/TestModels/SharedTestModels.cs index e2c7077..c22959e 100644 --- a/AyCode.Core.Tests/TestModels/SharedTestModels.cs +++ b/AyCode.Core.Tests/TestModels/SharedTestModels.cs @@ -218,7 +218,12 @@ public class TestPallet : IId // Level 4 collection public List 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 // Level 5 collection public List 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 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] diff --git a/AyCode.Core.Tests/TestModels/TestDataFactory.cs b/AyCode.Core.Tests/TestModels/TestDataFactory.cs index 71f4b6c..028bf84 100644 --- a/AyCode.Core.Tests/TestModels/TestDataFactory.cs +++ b/AyCode.Core.Tests/TestModels/TestDataFactory.cs @@ -104,7 +104,8 @@ public static class TestDataFactory #region Hierarchy Creation (5 Levels) /// - /// 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. /// 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 } /// - /// Create an order item with pallets + /// Create an order item with pallets. + /// Supports both IId-based and Non-IId shared references. /// 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; } + + + /// /// Create a pallet with measurements /// 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 /// /// Create a measurement with points /// - 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 /// /// Create a measurement point /// - 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 }; } diff --git a/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.cs b/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.cs index bd7768c..44fb88b 100644 --- a/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.cs +++ b/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.cs @@ -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 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 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 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 + + /// + /// Registers an IId instance after populate - uses pre-bound delegate, NO SWITCH! + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void RegisterIIdInstance( + BinaryDeserializationContextClass contextClass, + TypeMetadataWrapper wrapper, + object instance, + BinaryDeserializeTypeMetadata metadata) + { + // Pre-bound delegate - no switch needed! + wrapper.RegisterById!(instance); + } + + /// + /// Gets Id as boxed object - only used for ChainMode (rare path). + /// + [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)] diff --git a/AyCode.Core/Serializers/SerializationContextBase.cs b/AyCode.Core/Serializers/SerializationContextBase.cs index 21d08a5..e57eafc 100644 --- a/AyCode.Core/Serializers/SerializationContextBase.cs +++ b/AyCode.Core/Serializers/SerializationContextBase.cs @@ -40,8 +40,8 @@ public abstract class SerializationContextBase : AcSerializ { Debug.Assert(wrapper.Metadata.IdAccessorType == AcSerializerCommon.IdAccessorType.Int32); - var getter = (Func)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 : AcSerializ [MethodImpl(MethodImplOptions.AggressiveInlining)] private static bool TryTrackSmallId(TypeMetadataWrapper 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; @@ -65,6 +65,13 @@ public abstract class SerializationContextBase : AcSerializ word |= mask; return true; } + + [MethodImpl(MethodImplOptions.NoInlining)] + private static ulong[] InitSmallIdBitmap(TypeMetadataWrapper wrapper) + { + wrapper.SmallIdBitmap = new ulong[BitArraySize]; + return wrapper.SmallIdBitmap; + } /// /// Tries to track an object with long RefId. @@ -76,8 +83,8 @@ public abstract class SerializationContextBase : AcSerializ { Debug.Assert(wrapper.Metadata.IdAccessorType == AcSerializerCommon.IdAccessorType.Int64); - var getter = (Func)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 : AcSerializ { Debug.Assert(wrapper.Metadata.IdAccessorType == AcSerializerCommon.IdAccessorType.Guid); - var getter = (Func)wrapper.RefIdGetter; - refId = getter(obj); + // Use pre-cast getter - no cast overhead! + refId = wrapper.RefIdGetterGuid!(obj); return wrapper.TryAddKey(refId); } diff --git a/AyCode.Core/Serializers/TypeMetadataWrapper.cs b/AyCode.Core/Serializers/TypeMetadataWrapper.cs index a1c09b7..4cb9218 100644 --- a/AyCode.Core/Serializers/TypeMetadataWrapper.cs +++ b/AyCode.Core/Serializers/TypeMetadataWrapper.cs @@ -8,7 +8,7 @@ namespace AyCode.Core.Serializers; /// /// 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. /// /// The concrete metadata type (BinarySerializeTypeMetadata, etc.) public sealed class TypeMetadataWrapper where TMetadata : TypeMetadataBase @@ -17,22 +17,47 @@ public sealed class TypeMetadataWrapper where TMetadata : TypeMetadat /// The cached metadata reference (from GlobalMetadataCache). /// public readonly TMetadata Metadata; - + /// - /// Typed getter for reference ID. Runtime type is Func<object, int/long/Guid>. - /// 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. /// - internal readonly Delegate RefIdGetter; - + internal readonly Func? RefIdGetterInt32; + /// - /// Identity map for tracking. Runtime type is IdentityMap<int/long/Guid>. + /// Pre-cast Int64 getter for fast path. + /// Set when IdAccessorType == Int64. /// - protected AcSerializerCommon.IIdentityMap? IdentityMap; + internal readonly Func? RefIdGetterInt64; + + /// + /// Pre-cast Guid getter for fast path. + /// Set when IdAccessorType == Guid. + /// + internal readonly Func? RefIdGetterGuid; + + #region Typed IdentityMaps - No generic type checks in hot path! + + /// + /// Typed IdentityMap for Int32 IDs. Direct access, no type check. + /// + internal AcSerializerCommon.IdentityMap? IdentityMapInt32; + + /// + /// Typed IdentityMap for Int64 IDs. Direct access, no type check. + /// + internal AcSerializerCommon.IdentityMap? IdentityMapInt64; + + /// + /// Typed IdentityMap for Guid IDs. Direct access, no type check. + /// + internal AcSerializerCommon.IdentityMap? IdentityMapGuid; + + + #endregion /// - /// BitArray for tracking small int32 IDs (0-65535). + /// BitArray for tracking small int32 IDs (0-65535) during serialization. /// Only used when IdAccessorType == Int32. /// internal ulong[]? SmallIdBitmap; @@ -44,17 +69,41 @@ public sealed class TypeMetadataWrapper where TMetadata : TypeMetadat /// Shared across all wrappers to avoid allocation. /// private static readonly Func HashCodeGetter = RuntimeHelpers.GetHashCode; + + /// + /// Pre-bound delegate to register instance by Id - NO SWITCH in hot path! + /// Set in constructor based on IdAccessorType. Method group conversion = no allocation. + /// + internal readonly Action? RegisterById; /// /// 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. /// 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)refIdGetter; + RegisterById = RegisterByInt32Id; // Method group - no allocation! + break; + case AcSerializerCommon.IdAccessorType.Int64: + RefIdGetterInt64 = (Func)refIdGetter; + RegisterById = RegisterByInt64Id; + break; + case AcSerializerCommon.IdAccessorType.Guid: + RefIdGetterGuid = (Func)refIdGetter; + RegisterById = RegisterByGuidId; + break; + } } [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -65,7 +114,7 @@ public sealed class TypeMetadataWrapper where TMetadata : TypeMetadat /// /// Resets tracking state for reuse between serializations. - /// Does not deallocate - just clears for reuse. + /// Does not deallocate - just clears for reuse (pool-friendly). /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public void ResetTracking() @@ -73,31 +122,149 @@ public sealed class TypeMetadataWrapper 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 + /// - /// Ensures SmallIdBitmap is allocated (lazy allocation). + /// Tries to get object by Int32 Id. Returns true if found. + /// Direct dictionary lookup - no generic type check! /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal void EnsureSmallIdBitmap() + public bool TryGetValueInt32(int id, out object? instance) { - SmallIdBitmap ??= new ulong[BitArraySize]; + if (IdentityMapInt32 != null) + return IdentityMapInt32.TryGetValue(id, out instance); + + instance = null; + return false; } + + /// + /// Gets existing object or stores new one by Int32 Id. + /// Direct dictionary access - no generic type check! + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public object TryGetOrStoreInt32(int id, object newObj) + { + if (id == 0) return newObj; // Default Id - no tracking + + var map = IdentityMapInt32 ??= new AcSerializerCommon.IdentityMap(); + return map.TryGetOrAddValue(id, newObj); + } + + /// + /// Registers IId instance by extracting its Int32 Id. + /// Combines Id getter + store in one call. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void RegisterByInt32Id(object instance) + { + var id = RefIdGetterInt32!(instance); + if (id == 0) return; + + var map = IdentityMapInt32 ??= new AcSerializerCommon.IdentityMap(); + 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(); + 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(); + 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(); + 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(); + map.TryGetOrAddValue(id, instance); + } + + #endregion + + #region Legacy Generic Methods (for backward compatibility) + /// /// Gets or creates the typed IdentityMap for tracking. - /// Use: wrapper.GetOrCreateIdentityMap<int>().TryAddKey(id) + /// SLOW PATH - use typed methods (TryGetValueInt32, etc.) instead! /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public AcSerializerCommon.IdentityMap GetOrCreateIdentityMap() where TId : notnull { - if (IdentityMap is AcSerializerCommon.IdentityMap typedMap) - return typedMap; - - var newMap = new AcSerializerCommon.IdentityMap(); - IdentityMap = newMap; - return newMap; + // Route to typed fields based on TId + if (typeof(TId) == typeof(int)) + { + var map = IdentityMapInt32 ??= new AcSerializerCommon.IdentityMap(); + return Unsafe.As, AcSerializerCommon.IdentityMap>(ref map); + } + if (typeof(TId) == typeof(long)) + { + var map = IdentityMapInt64 ??= new AcSerializerCommon.IdentityMap(); + return Unsafe.As, AcSerializerCommon.IdentityMap>(ref map); + } + if (typeof(TId) == typeof(Guid)) + { + var map = IdentityMapGuid ??= new AcSerializerCommon.IdentityMap(); + return Unsafe.As, AcSerializerCommon.IdentityMap>(ref map); + } + + throw new NotSupportedException($"Id type {typeof(TId)} is not supported"); } [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -117,9 +284,9 @@ public sealed class TypeMetadataWrapper where TMetadata : TypeMetadat [MethodImpl(MethodImplOptions.AggressiveInlining)] public object TryGetOrStoreId(TId id, object newObj) where TId : struct { - //if (id == default(TId)) return newObj; // Default Id - no tracking - var map = GetOrCreateIdentityMap(); return map.TryGetOrAddValue(id, newObj); } + + #endregion }