From 9b4fa1159a5879270fbf440c44c606a20fdd8ab1 Mon Sep 17 00:00:00 2001 From: Loretta Date: Fri, 6 Feb 2026 15:48:48 +0100 Subject: [PATCH] Optimize cache index assignment during scan pass Refactored AcBinarySerializer to assign cache indices immediately upon detecting duplicates during the scan pass, eliminating the need for a separate post-processing step. Updated TryTrack methods to take a ref nextCacheIndex for inline assignment. Removed AssignCacheIndicesInOrder and related code, simplified string interning, and made RegisterMetadataType static. This reduces allocations and improves performance by making cache index assignment a single-pass operation. --- ...rySerializer.BinarySerializationContext.cs | 238 ++---------------- .../Binaries/AcBinarySerializer.ScanPass.cs | 29 ++- .../Binaries/AcBinarySerializer.cs | 2 +- .../Serializers/SerializationContextBase.cs | 45 +--- .../Serializers/TypeMetadataWrapper.cs | 28 ++- 5 files changed, 59 insertions(+), 283 deletions(-) diff --git a/AyCode.Core/Serializers/Binaries/AcBinarySerializer.BinarySerializationContext.cs b/AyCode.Core/Serializers/Binaries/AcBinarySerializer.BinarySerializationContext.cs index 987590f..2ed0ab4 100644 --- a/AyCode.Core/Serializers/Binaries/AcBinarySerializer.BinarySerializationContext.cs +++ b/AyCode.Core/Serializers/Binaries/AcBinarySerializer.BinarySerializationContext.cs @@ -96,9 +96,18 @@ public static partial class AcBinarySerializer //private readonly AcSerializerCommon.SerializationReferenceTracker _refTracker = new(); private IdentityMap? _stringInternMap; - private int _nextCacheIndex; // Next dense cache index to assign + private int _nextCacheIndex; // Next dense cache index to assign (starts at 0, uses ++_nextCacheIndex) private int _nextFirstIndex; // Next first occurrence index to assign (scan pass) + /// + /// Next cache index reference for scan pass. Direct ref access for TryTrack methods. + /// + public ref int NextCacheIndexRef + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => ref _nextCacheIndex; + } + /// /// Next first occurrence index for scan pass. Direct access for performance. /// @@ -259,7 +268,7 @@ public static partial class AcBinarySerializer } /// - /// Scan pass: tracks a string for interning. Marks as cached on 2nd occurrence. + /// Scan pass: tracks a string for interning. Assigns CacheIndex immediately on 2nd occurrence. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public void ScanInternString(string value) @@ -268,14 +277,17 @@ public static partial class AcBinarySerializer if (!_stringInternMap.TryAdd(value, out var slotIndex)) { - // 2+ occurrence: mark as cached + // 2+ occurrence: assign CacheIndex immediately ref var entry = ref _stringInternMap.GetValueRef(slotIndex); if (entry.CacheIndex == -1) - entry.CacheIndex = -2; // -2 = cached, pending CacheIndex assignment + { + entry.CacheIndex = ++_nextCacheIndex; + entry.IsFirstWrite = true; + } return; } - // 1st occurrence: store FirstIndex + // 1st occurrence: store FirstIndex for validation, CacheIndex = -1 (not cached yet) ref var newEntry = ref _stringInternMap.GetValueRef(slotIndex); newEntry.FirstIndex = _nextFirstIndex++; newEntry.CacheIndex = -1; @@ -291,220 +303,6 @@ public static partial class AcBinarySerializer /// public int GetCacheCount() => _nextCacheIndex; - /// - /// Assigns CacheIndex values in FirstIndex order after scan pass. - /// Collects all cached entries (CacheIndex == -2), sorts by FirstIndex, assigns 0, 1, 2... - /// Optimized: single pass collection, no allocations for wrapper iteration. - /// - public void AssignCacheIndicesInOrder() - { - // Fast path: no caching at all - if (_stringInternMap == null && !HasAnyIdentityMap()) - { - return; - } - - // Count cached entries in single pass - var cachedCount = CountAllCachedEntries(); - if (cachedCount == 0) - return; - - // Collect entries for sorting - Span<(int SlotIndex, int FirstIndex, int MapType)> entries = cachedCount <= 64 - ? stackalloc (int, int, int)[cachedCount] - : new (int, int, int)[cachedCount]; - - var idx = 0; - - // Collect from string intern map (mapType = 0) - if (_stringInternMap != null) - { - var count = _stringInternMap.Count; - for (var i = 0; i < count; i++) - { - ref var entry = ref _stringInternMap.GetValueRefAt(i); - if (entry.CacheIndex == -2) - entries[idx++] = (i, entry.FirstIndex, 0); - } - } - - // Collect from wrapper identity maps - use foreach, no allocation - var wrapperIdx = 1; - foreach (var wrapper in GetWrappers()) - { - var baseMapType = wrapperIdx * 3; - CollectCachedEntries(wrapper.IdentityMapInt32, baseMapType + 0, ref entries, ref idx); - CollectCachedEntries(wrapper.IdentityMapInt64, baseMapType + 1, ref entries, ref idx); - CollectCachedEntries(wrapper.IdentityMapGuid, baseMapType + 2, ref entries, ref idx); - wrapperIdx++; - } - - // Sort by FirstIndex - var usedEntries = entries.Slice(0, idx); - usedEntries.Sort((a, b) => a.FirstIndex.CompareTo(b.FirstIndex)); - - // Assign CacheIndex in sorted order - for (var i = 0; i < idx; i++) - { - var (slotIndex, _, mapType) = usedEntries[i]; - if (mapType == 0) - { - ref var entry = ref _stringInternMap!.GetValueRefAt(slotIndex); - entry.CacheIndex = _nextCacheIndex++; - entry.IsFirstWrite = true; - } - else - { - // Find wrapper by index - var wrapperIndex = mapType / 3 - 1; - var mapIndex = mapType % 3; - var wIdx = 0; - foreach (var wrapper in GetWrappers()) - { - if (wIdx == wrapperIndex) - { - switch (mapIndex) - { - case 0: - ref var entry32 = ref wrapper.IdentityMapInt32!.GetValueRefAt(slotIndex); - entry32.CacheIndex = _nextCacheIndex++; - entry32.IsFirstWrite = true; - break; - case 1: - ref var entry64 = ref wrapper.IdentityMapInt64!.GetValueRefAt(slotIndex); - entry64.CacheIndex = _nextCacheIndex++; - entry64.IsFirstWrite = true; - break; - case 2: - ref var entryGuid = ref wrapper.IdentityMapGuid!.GetValueRefAt(slotIndex); - entryGuid.CacheIndex = _nextCacheIndex++; - entryGuid.IsFirstWrite = true; - break; - } - break; - } - wIdx++; - } - } - } - -#if DEBUG - // DEBUG: Print string intern map contents - if (_stringInternMap != null) - { - Console.WriteLine($"\n=== AssignCacheIndicesInOrder completed ==="); - Console.WriteLine($"Total strings in map: {_stringInternMap.Count}"); - Console.WriteLine($"Total cached (CacheIndex >= 0): {cachedCount}"); - Console.WriteLine($"NextCacheIndex: {_nextCacheIndex}"); - Console.WriteLine("String entries:"); - for (var i = 0; i < _stringInternMap.Count; i++) - { - ref var entry = ref _stringInternMap.GetValueRefAt(i); - var key = _stringInternMap.GetKeyAt(i); - Console.WriteLine($" [{i}] Key=\"{key}\" FirstIndex={entry.FirstIndex} CacheIndex={entry.CacheIndex} IsFirstWrite={entry.IsFirstWrite}"); - } - Console.WriteLine(); - } -#endif - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private bool HasAnyIdentityMap() - { - foreach (var wrapper in GetWrappers()) - { - if (wrapper.IdentityMapInt32 != null || wrapper.IdentityMapInt64 != null || wrapper.IdentityMapGuid != null) - return true; - } - return false; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private int CountAllCachedEntries() - { - var cachedCount = 0; - if (_stringInternMap != null) - { - var count = _stringInternMap.Count; - for (var i = 0; i < count; i++) - { - ref var entry = ref _stringInternMap.GetValueRefAt(i); - if (entry.CacheIndex == -2) - cachedCount++; - } - } - foreach (var wrapper in GetWrappers()) - { - cachedCount += CountCachedEntries(wrapper.IdentityMapInt32); - cachedCount += CountCachedEntries(wrapper.IdentityMapInt64); - cachedCount += CountCachedEntries(wrapper.IdentityMapGuid); - } - return cachedCount; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static int CountCachedEntries(IdentityMap? map) where TKey : notnull - { - if (map == null) return 0; - var count = 0; - var mapCount = map.Count; - for (var i = 0; i < mapCount; i++) - { - ref var entry = ref map.GetValueRefAt(i); - if (entry.CacheIndex == -2) - count++; - } - return count; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void CollectCachedEntries( - IdentityMap? map, - int mapType, - ref Span<(int SlotIndex, int FirstIndex, int MapType)> entries, - ref int idx) where TKey : notnull - { - if (map == null) return; - var count = map.Count; - for (var i = 0; i < count; i++) - { - ref var entry = ref map.GetValueRefAt(i); - if (entry.CacheIndex == -2) - entries[idx++] = (i, entry.FirstIndex, mapType); - } - } - - #endregion - - #region Object Reference Tracking (IId + Non-IId) - - /// - /// Tries to track an IId object (Int32 Id). - /// Returns true if first occurrence, false if already seen. - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public bool TryTrackObject(TypeMetadataWrapper wrapper, object obj, out int cacheIndex) - { - return TryTrack(wrapper, obj, _nextFirstIndex++, out cacheIndex); - } - - /// - /// Tries to track an IId object (Int64 Id). - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public bool TryTrackObjectLong(TypeMetadataWrapper wrapper, object obj, out int cacheIndex) - { - return TryTrackLong(wrapper, obj, _nextFirstIndex++, out cacheIndex); - } - - /// - /// Tries to track an IId object (Guid Id). - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public bool TryTrackObjectGuid(TypeMetadataWrapper wrapper, object obj, out int cacheIndex) - { - return TryTrackGuid(wrapper, obj, _nextFirstIndex++, out cacheIndex); - } #endregion @@ -516,7 +314,7 @@ public static partial class AcBinarySerializer /// false-t ha ismételt (csak propNameHash kell). /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public bool RegisterMetadataType(TypeMetadataWrapper wrapper) + public static bool RegisterMetadataType(TypeMetadataWrapper wrapper) { if (wrapper.MetadataFooterIndex >= 0) return false; // ismételt diff --git a/AyCode.Core/Serializers/Binaries/AcBinarySerializer.ScanPass.cs b/AyCode.Core/Serializers/Binaries/AcBinarySerializer.ScanPass.cs index 5a76731..a4ed916 100644 --- a/AyCode.Core/Serializers/Binaries/AcBinarySerializer.ScanPass.cs +++ b/AyCode.Core/Serializers/Binaries/AcBinarySerializer.ScanPass.cs @@ -9,7 +9,7 @@ public static partial class AcBinarySerializer /// First pass: scans object graph to identify duplicates (strings + objects). /// Only traverses reference properties (complex types + strings). /// Stops traversing an object after its 2nd occurrence. - /// After scan: assigns CacheIndex in FirstIndex order. + /// CacheIndex is assigned immediately on 2nd occurrence (no post-processing needed). /// private static void ScanForDuplicates(object value, Type type, BinarySerializationContext context) { @@ -17,7 +17,7 @@ public static partial class AcBinarySerializer return; ScanValue(value, type, context, 0); - context.AssignCacheIndicesInOrder(); + // No AssignCacheIndicesInOrder() needed - CacheIndex assigned inline on 2nd occurrence } private static void ScanValue(object? value, Type type, BinarySerializationContext context, int depth) @@ -70,15 +70,15 @@ public static partial class AcBinarySerializer { case IdAccessorType.Int32: var id32 = wrapper.RefIdGetterInt32!(value); - isFirst = wrapper.TryTrackInt32(id32, context.NextFirstIndex++, out _); + isFirst = wrapper.TryTrackInt32(id32, context.NextFirstIndex++, ref context.NextCacheIndexRef, out _); break; case IdAccessorType.Int64: var id64 = wrapper.RefIdGetterInt64!(value); - isFirst = wrapper.TryTrackInt64(id64, context.NextFirstIndex++, out _); + isFirst = wrapper.TryTrackInt64(id64, context.NextFirstIndex++, ref context.NextCacheIndexRef, out _); break; case IdAccessorType.Guid: var idGuid = wrapper.RefIdGetterGuid!(value); - isFirst = wrapper.TryTrackGuid(idGuid, context.NextFirstIndex++, out _); + isFirst = wrapper.TryTrackGuid(idGuid, context.NextFirstIndex++, ref context.NextCacheIndexRef, out _); break; default: isFirst = true; @@ -90,13 +90,26 @@ public static partial class AcBinarySerializer } // Recursive scan on reference properties only + // Use typed getter for strings (much faster than reflection GetValue) var refProperties = metadata.ReferenceProperties; var nextDepth2 = depth + 1; for (var i = 0; i < refProperties.Length; i++) { - var propValue = refProperties[i].GetValue(value); - if (propValue != null) - ScanValue(propValue, refProperties[i].PropertyType, context, nextDepth2); + var prop = refProperties[i]; + if (prop.AccessorType == PropertyAccessorType.String) + { + // Fast path: typed getter for string + var str2 = prop.GetString(value); + if (str2 != null && context.IsValidForInterningString(str2.Length)) + context.ScanInternString(str2); + } + else + { + // Object property: use generic getter + var propValue = prop.GetValue(value); + if (propValue != null) + ScanValue(propValue, prop.PropertyType, context, nextDepth2); + } } } } diff --git a/AyCode.Core/Serializers/Binaries/AcBinarySerializer.cs b/AyCode.Core/Serializers/Binaries/AcBinarySerializer.cs index a76092d..4ebdd40 100644 --- a/AyCode.Core/Serializers/Binaries/AcBinarySerializer.cs +++ b/AyCode.Core/Serializers/Binaries/AcBinarySerializer.cs @@ -799,7 +799,7 @@ public static partial class AcBinarySerializer var isFirstMetadataOccurrence = false; if (context.UseMetadata) { - isFirstMetadataOccurrence = context.RegisterMetadataType(wrapper); + isFirstMetadataOccurrence = BinarySerializationContext.RegisterMetadataType(wrapper); } // Reference handling: lookup entry from scan pass, check IsFirstWrite diff --git a/AyCode.Core/Serializers/SerializationContextBase.cs b/AyCode.Core/Serializers/SerializationContextBase.cs index 931ad18..a1127e4 100644 --- a/AyCode.Core/Serializers/SerializationContextBase.cs +++ b/AyCode.Core/Serializers/SerializationContextBase.cs @@ -1,12 +1,8 @@ -using System; -using System.Diagnostics; -using System.Runtime.CompilerServices; - namespace AyCode.Core.Serializers; /// /// Abstract base class for all serialization contexts (Binary, JSON, Toon). -/// Provides common serialization operations: Id extraction, tracking first occurrence. +/// Provides common serialization operations. /// Derived classes are sealed for JIT devirtualization (direct call speed). /// /// The concrete metadata type for serialization. @@ -15,45 +11,6 @@ public abstract class SerializationContextBase : AcSerializ where TMetadata : TypeMetadataBase where TOptions : AcSerializerOptions { - - #region Tracking - InternEntry based (serializer side) - - /// - /// Tries to track an object with int RefId. - /// Returns true if first occurrence, false if already seen. - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public bool TryTrack(TypeMetadataWrapper wrapper, object obj, int firstIndex, out int cacheIndex) - { - Debug.Assert(wrapper.Metadata.IdAccessorType == IdAccessorType.Int32); - var id = wrapper.RefIdGetterInt32!(obj); - return wrapper.TryTrackInt32(id, firstIndex, out cacheIndex); - } - - /// - /// Tries to track an object with long RefId. - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public bool TryTrackLong(TypeMetadataWrapper wrapper, object obj, int firstIndex, out int cacheIndex) - { - Debug.Assert(wrapper.Metadata.IdAccessorType == IdAccessorType.Int64); - var id = wrapper.RefIdGetterInt64!(obj); - return wrapper.TryTrackInt64(id, firstIndex, out cacheIndex); - } - - /// - /// Tries to track an object with Guid RefId. - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public bool TryTrackGuid(TypeMetadataWrapper wrapper, object obj, int firstIndex, out int cacheIndex) - { - Debug.Assert(wrapper.Metadata.IdAccessorType == IdAccessorType.Guid); - var id = wrapper.RefIdGetterGuid!(obj); - return wrapper.TryTrackGuid(id, firstIndex, out cacheIndex); - } - - #endregion - #region Reset /// diff --git a/AyCode.Core/Serializers/TypeMetadataWrapper.cs b/AyCode.Core/Serializers/TypeMetadataWrapper.cs index b1f5641..cd40fae 100644 --- a/AyCode.Core/Serializers/TypeMetadataWrapper.cs +++ b/AyCode.Core/Serializers/TypeMetadataWrapper.cs @@ -140,26 +140,28 @@ public sealed class TypeMetadataWrapper where TMetadata : TypeMetadat /// /// Tries to track Int32 Id. Returns true if first occurrence. - /// On 2+ occurrence: marks as cached (-2), returns existing CacheIndex. - /// CacheIndex is assigned later by AssignCacheIndicesInOrder(). + /// On 2+ occurrence: assigns CacheIndex immediately using ++nextCacheIndex. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public bool TryTrackInt32(int id, int firstIndex, out int cacheIndex) + public bool TryTrackInt32(int id, int firstIndex, ref int nextCacheIndex, out int cacheIndex) { if (id == 0) { cacheIndex = -1; return true; } // Default Id - no tracking var map = IdentityMapInt32 ??= new IdentityMap(); if (!map.TryAdd(id, out var slotIndex)) { - // 2+ occurrence: mark as cached + // 2+ occurrence: assign CacheIndex immediately ref var entry = ref map.GetValueRef(slotIndex); if (entry.CacheIndex == -1) - entry.CacheIndex = -2; // -2 = cached, pending assignment + { + entry.CacheIndex = ++nextCacheIndex; + entry.IsFirstWrite = true; + } cacheIndex = entry.CacheIndex; return false; } - // 1st occurrence: store FirstIndex + // 1st occurrence: store FirstIndex for validation ref var newEntry = ref map.GetValueRef(slotIndex); newEntry.FirstIndex = firstIndex; newEntry.CacheIndex = -1; @@ -171,7 +173,7 @@ public sealed class TypeMetadataWrapper where TMetadata : TypeMetadat /// Tries to track Int64 Id. Returns true if first occurrence. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public bool TryTrackInt64(long id, int firstIndex, out int cacheIndex) + public bool TryTrackInt64(long id, int firstIndex, ref int nextCacheIndex, out int cacheIndex) { if (id == 0) { cacheIndex = -1; return true; } @@ -180,7 +182,10 @@ public sealed class TypeMetadataWrapper where TMetadata : TypeMetadat { ref var entry = ref map.GetValueRef(slotIndex); if (entry.CacheIndex == -1) - entry.CacheIndex = -2; + { + entry.CacheIndex = ++nextCacheIndex; + entry.IsFirstWrite = true; + } cacheIndex = entry.CacheIndex; return false; } @@ -196,7 +201,7 @@ public sealed class TypeMetadataWrapper where TMetadata : TypeMetadat /// Tries to track Guid Id. Returns true if first occurrence. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public bool TryTrackGuid(Guid id, int firstIndex, out int cacheIndex) + public bool TryTrackGuid(Guid id, int firstIndex, ref int nextCacheIndex, out int cacheIndex) { if (id == Guid.Empty) { cacheIndex = -1; return true; } @@ -205,7 +210,10 @@ public sealed class TypeMetadataWrapper where TMetadata : TypeMetadat { ref var entry = ref map.GetValueRef(slotIndex); if (entry.CacheIndex == -1) - entry.CacheIndex = -2; + { + entry.CacheIndex = ++nextCacheIndex; + entry.IsFirstWrite = true; + } cacheIndex = entry.CacheIndex; return false; }