diff --git a/AyCode.Core/Serializers/Binaries/AcBinarySerializer.BinarySerializationContext.cs b/AyCode.Core/Serializers/Binaries/AcBinarySerializer.BinarySerializationContext.cs index 4538146..212c74c 100644 --- a/AyCode.Core/Serializers/Binaries/AcBinarySerializer.BinarySerializationContext.cs +++ b/AyCode.Core/Serializers/Binaries/AcBinarySerializer.BinarySerializationContext.cs @@ -97,17 +97,7 @@ public static partial class AcBinarySerializer // Use shared reference tracker from AcSerializerCommon //private readonly AcSerializerCommon.SerializationReferenceTracker _refTracker = new(); - /// - /// String intern entry for tracking string occurrences. - /// StreamPosition-based approach for 100% reliable cache matching. - /// - private struct StringInternEntry - { - public int StreamPosition; // Position in stream where string was first written - public int CacheIndex; // Dense cache index (0, 1, 2, ...) - assigned at 2nd occurrence; -1 = first occurrence only - } - - private Dictionary? _stringInternMap; + private IdentityMap? _stringInternMap; private int _nextCacheIndex; // Next dense cache index to assign private Dictionary? _propertyNames; @@ -181,7 +171,7 @@ public static partial class AcBinarySerializer _position = 0; //_refTracker.Reset(); - ClearAndTrimIfNeeded(_stringInternMap, InitialInternCapacity * 4); + _stringInternMap?.Reset(); ClearAndTrimIfNeeded(_propertyNames, InitialPropertyNameCapacity * 4); _propertyNameList?.Clear(); @@ -240,13 +230,12 @@ public static partial class AcBinarySerializer [MethodImpl(MethodImplOptions.AggressiveInlining)] public bool TryGetInternedString(string value, int streamPosition, out int cacheIndex) { - _stringInternMap ??= new Dictionary(InitialInternCapacity, StringComparer.Ordinal); + _stringInternMap ??= new IdentityMap(); - ref var entry = ref CollectionsMarshal.GetValueRefOrNullRef(_stringInternMap, value); - - if (!Unsafe.IsNullRef(ref entry)) + if (!_stringInternMap.TryAdd(value, out var slotIndex)) { // 2+ occurrence: assign CacheIndex if first repeat + ref var entry = ref _stringInternMap.GetValueRef(slotIndex); if (entry.CacheIndex < 0) { entry.CacheIndex = _nextCacheIndex++; @@ -256,11 +245,9 @@ public static partial class AcBinarySerializer } // 1st occurrence: store stream position - _stringInternMap[value] = new StringInternEntry - { - StreamPosition = streamPosition, - CacheIndex = -1 // Not assigned until 2nd occurrence - }; + ref var newEntry = ref _stringInternMap.GetValueRef(slotIndex); + newEntry.StreamPosition = streamPosition; + newEntry.CacheIndex = -1; // Not assigned until 2nd occurrence cacheIndex = -1; return false; } @@ -268,7 +255,7 @@ public static partial class AcBinarySerializer /// /// Returns true if there are any interned strings that occurred more than once. /// - public bool HasInternedStrings => _stringInternMap is { Count: > 0 }; + public bool HasInternedStrings => _stringInternMap != null && _stringInternMap.Count > 0; /// /// Gets the count of strings that occurred more than once (for footer). @@ -290,8 +277,10 @@ public static partial class AcBinarySerializer : new (int, int)[_nextCacheIndex]; var idx = 0; - foreach (var entry in _stringInternMap.Values) + var count = _stringInternMap.Count; + for (var i = 0; i < count; i++) { + ref var entry = ref _stringInternMap.GetValueRefAt(i); if (entry.CacheIndex >= 0) { entries[idx++] = (entry.StreamPosition, entry.CacheIndex); diff --git a/AyCode.Core/Serializers/IdentityMap.cs b/AyCode.Core/Serializers/IdentityMap.cs index 5b6eddb..9108c01 100644 --- a/AyCode.Core/Serializers/IdentityMap.cs +++ b/AyCode.Core/Serializers/IdentityMap.cs @@ -22,6 +22,18 @@ public enum IdAccessorType : byte /// Id is Guid. Guid = 3, } +/// +/// Common entry for tracking interned values (strings and IId objects) during serialization. +/// Used as TValue in IdentityMap<TKey, InternEntry>. +/// +public struct InternEntry +{ + /// Position in stream where the value was first written. + public int StreamPosition; + /// Dense cache index (0, 1, 2, ...) assigned at 2nd occurrence. -1 = first occurrence only. + public int CacheIndex; +} + /// /// Interface for identity maps used in serialization tracking. /// Enables type-safe Reset() without knowing the generic type parameter. @@ -111,6 +123,23 @@ public sealed class IdentityMap : IIdentityMap where TKey : notnul return TryAddHash(key, out _); } + /// + /// Tries to add a key and returns slot index for ref access to value. + /// Returns true if first occurrence (key was added). + /// Returns false if already seen. + /// Use GetValueRef(slotIndex) to read/write the value. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool TryAdd(TKey key, out int slotIndex) + { + return TryAddHash(key, out slotIndex); + } + + /// + /// Number of entries in the hash table. Use with GetValueRefAt for iteration. + /// + public int Count => _count; + [MethodImpl(MethodImplOptions.AggressiveInlining)] private bool TryAddSmallInt(int key) { @@ -360,7 +389,7 @@ public sealed class IdentityMap : IIdentityMap where TKey : notnul /// /// Returns a reference to the value at the given slot index. - /// Use with slotIndex from TryAddHash for in-place value modification. + /// Use with slotIndex from TryAdd for in-place value modification. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public ref TValue GetValueRef(int slotIndex) @@ -368,6 +397,16 @@ public sealed class IdentityMap : IIdentityMap where TKey : notnul return ref _entries![slotIndex].Value; } + /// + /// Returns the value at the given sequential index (0..Count-1). + /// For iteration over all entries (e.g., footer writing). + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public ref TValue GetValueRefAt(int index) + { + return ref _entries![index].Value; + } + /// /// Resets the identity map for reuse. /// Small arrays (≤ InitialHashCapacity*5): keep and clear (faster than pool round-trip).