From 9b151fd6cf7675f63474d65d569e2ff83ae5e3f7 Mon Sep 17 00:00:00 2001 From: Loretta Date: Sun, 1 Feb 2026 12:18:27 +0100 Subject: [PATCH] Unify string interning and object reference tracking Major refactor: merges string interning and object reference tracking (IId/Non-IId) into a unified position-based cache and footer in binary serialization. Wire format now uses cache indices for all references; hashcode/Id prefixes removed. Serialization and deserialization logic simplified, improving performance and maintainability. Legacy code paths and redundant dictionary lookups eliminated. --- .../Serializers/AcSerializerContextBase.cs | 47 +--- ...serializer.BinaryDeserializationContext.cs | 101 +++---- ...lizer.BinaryDeserializationContextClass.cs | 23 -- .../AcBinaryDeserializer.CrossType.cs | 41 +-- .../Binaries/AcBinaryDeserializer.Populate.cs | 34 +-- .../Binaries/AcBinaryDeserializer.cs | 169 ++---------- ...rySerializer.BinarySerializationContext.cs | 128 ++++++--- .../Binaries/AcBinarySerializer.cs | 31 +-- AyCode.Core/Serializers/IdentityMap.cs | 257 ++++-------------- .../Serializers/SerializationContextBase.cs | 76 +----- .../Serializers/TypeMetadataWrapper.cs | 229 +++++----------- 11 files changed, 325 insertions(+), 811 deletions(-) diff --git a/AyCode.Core/Serializers/AcSerializerContextBase.cs b/AyCode.Core/Serializers/AcSerializerContextBase.cs index b0a77f5..91a7755 100644 --- a/AyCode.Core/Serializers/AcSerializerContextBase.cs +++ b/AyCode.Core/Serializers/AcSerializerContextBase.cs @@ -83,54 +83,13 @@ public abstract class AcSerializerContextBase #endregion - #region Tracking API - int - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public bool TryGetValue(TypeMetadataWrapper wrapper, int refId, out object? instance) - { - return wrapper.TryGetValue(refId, out instance); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public bool TryGetValue(TypeMetadataWrapper wrapper, long refId, out object? instance) - { - return wrapper.TryGetValue(refId, out instance); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public bool TryGetValue(TypeMetadataWrapper wrapper, Guid refId, out object? instance) - { - return wrapper.TryGetValue(refId, out instance); - } + #region Wrapper Iteration (for footer writing) /// - /// For deserialization: checks if an object with this Id was already seen. - /// If yes, returns the existing object. If no, stores this object and returns it. - /// Uses IdentityMap.TryGetOrAddValue - single Dictionary operation! + /// Iterates all wrappers and collects InternEntry data for ID tracking footer. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public object TryGetOrStoreInt32(TypeMetadataWrapper wrapper, object newObj, int id) - { - return id == 0 ? newObj : wrapper.TryGetOrStoreId(id, newObj); - } - - /// - /// For deserialization: checks if an object with this Id was already seen (long version). - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public object TryGetOrStoreLong(TypeMetadataWrapper wrapper, object newObj, long id) - { - return id == 0 ? newObj : wrapper.TryGetOrStoreId(id, newObj); - } - - /// - /// For deserialization: checks if an object with this Id was already seen (Guid version). - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public object TryGetOrStoreGuid(TypeMetadataWrapper wrapper, object newObj, Guid id) - { - return id == Guid.Empty ? newObj : wrapper.TryGetOrStoreId(id, newObj); - } + public Dictionary>.ValueCollection GetWrappers() => _wrappers.Values; #endregion diff --git a/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.BinaryDeserializationContext.cs b/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.BinaryDeserializationContext.cs index 25fe89b..486952a 100644 --- a/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.BinaryDeserializationContext.cs +++ b/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.BinaryDeserializationContext.cs @@ -20,10 +20,11 @@ public static partial class AcBinaryDeserializer private List? _propertyNames; private Dictionary? _stringCache; - // Position-based string interning: flat int[] for cache-friendly access + // Position-based interning: flat int[] for cache-friendly access // Layout: [pos0, cacheIdx0, pos1, cacheIdx1, ...] - pairs sorted by position + // Shared for string interning AND IId object references private int[]? _dupData; // Footer data: (position, cacheIndex) pairs as flat int array - private string[]? _internStringCache; // Cache for duplicated strings only + private object?[]? _internCache; // Shared cache for interned strings AND IId objects private int _dupCheckIndex; // Current index in _dupData (increments by 2) private int _nextDupPosition; // Cached next dup position - avoids array access in hot path @@ -76,9 +77,9 @@ public static partial class AcBinaryDeserializer _propertyNames = null; _stringCache = null; - // Position-based string interning fields + // Position-based interning fields (shared: string + IId) _dupData = null; - _internStringCache = null; + _internCache = null; _dupCheckIndex = 0; _nextDupPosition = int.MaxValue; @@ -160,18 +161,19 @@ public static partial class AcBinaryDeserializer } } - // Footer-based: read string intern indices from footer + // Footer-based: read intern indices from footer (string + IId) if (hasInternFooter && footerPosition > 0) { - ReadFooterStringIndices(footerPosition); + ReadFooterIndices(footerPosition); } } /// - /// Reads string intern footer: [dupCount][pos0][idx0][pos1][idx1]... + /// Reads intern footer: [dupCount][pos0][idx0][pos1][idx1]... + /// Shared for string interning AND IId object references. /// VarUInt format read into flat int[] for fast hot path access. /// - private void ReadFooterStringIndices(int footerPosition) + private void ReadFooterIndices(int footerPosition) { // Save current position (start of data) var dataPosition = _position; @@ -184,7 +186,7 @@ public static partial class AcBinaryDeserializer if (dupCount == 0) { _dupData = Array.Empty(); - _internStringCache = Array.Empty(); + _internCache = Array.Empty(); _nextDupPosition = int.MaxValue; } else @@ -198,7 +200,7 @@ public static partial class AcBinaryDeserializer _dupData[i * 2 + 1] = (int)ReadVarUInt(); // cacheIndex } - _internStringCache = new string[dupCount]; + _internCache = new object?[dupCount]; // Cache first dup position for ultra-fast hot path _nextDupPosition = _dupData[0]; } @@ -555,14 +557,14 @@ public static partial class AcBinaryDeserializer } /// - /// Registers an interned string during body read (StringInternNew). + /// Registers an interned value (string or IId object) during body read. /// Uses position-based check for 100% reliable cache matching. /// Ultra-fast: single int comparison in hot path. /// - /// The string value read from stream - /// Stream position BEFORE reading the string (type code position) + /// The value read from stream (string or IId object) + /// Stream position BEFORE reading the value (type code position) [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void RegisterInternedString(string value, int streamPosition) + public void RegisterInternedValue(object value, int streamPosition) { // Ultra-fast hot path: single int comparison if (streamPosition != _nextDupPosition) @@ -572,7 +574,7 @@ public static partial class AcBinaryDeserializer // _dupData layout: [pos0, cacheIdx0, pos1, cacheIdx1, ...] var data = _dupData!; var idx = _dupCheckIndex; - _internStringCache![data[idx + 1]] = value; // cacheIndex is at odd positions + _internCache![data[idx + 1]] = value; // cacheIndex is at odd positions idx += 2; _dupCheckIndex = idx; @@ -587,12 +589,7 @@ public static partial class AcBinaryDeserializer [MethodImpl(MethodImplOptions.AggressiveInlining)] public string GetInternedString(int cacheIndex) { - //if (_internStringCache == null || cacheIndex >= _internStringCache.Length) - //{ - // throw new AcBinaryDeserializationException($"Invalid interned string cache index '{cacheIndex}'.", _position); - //} - - var result = _internStringCache![cacheIndex]; + var result = _internCache![cacheIndex]; if (result == null) { throw new AcBinaryDeserializationException( @@ -600,6 +597,23 @@ public static partial class AcBinaryDeserializer _position); } + return (string)result; + } + + /// + /// Gets an interned object by cache index (ObjectRef type code). + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public object GetInternedObject(int cacheIndex) + { + var result = _internCache![cacheIndex]; + if (result == null) + { + throw new AcBinaryDeserializationException( + $"Interned object at cache index '{cacheIndex}' was not populated.", + _position); + } + return result; } @@ -614,37 +628,6 @@ public static partial class AcBinaryDeserializer return _propertyNames[index]; } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void RegisterObject(TypeMetadataWrapper wrapper, int refId, object instance) - { - if (refId == 0) throw new Exception("refId == 0"); - ContextClass.TryGetOrStoreInt32(wrapper, instance, refId); - //if (refId <= 0) - //{ - // return; - //} - - //_objectReferences ??= new Dictionary(16); - //_objectReferences[refId] = instance; - } - - //[MethodImpl(MethodImplOptions.AggressiveInlining)] - //public object? GetReferencedObject(TypeMetadataWrapper wrapper, int refId) - //{ - // //if (refId <= 0) - // //{ - // // return null; - // //} - - // //if (_objectReferences == null || !_objectReferences.TryGetValue(refId, out var value)) - // //{ - // // throw new AcBinaryDeserializationException($"Unknown object reference id '{refId}'.", _position); - // //} - - - // //return value; - //} - private void EnsureAvailable(int length) { if (_position > _buffer.Length - length) @@ -659,19 +642,5 @@ public static partial class AcBinaryDeserializer return ReadStringUtf8(byteLength); } - #region IId Reference Cache - Delegates to ContextClass - - /// - /// After PopulateObject, checks if we should reuse an existing IId object. - /// Delegates to ContextClass which uses AcSerializerContextBase infrastructure. - /// Returns the object to use (either the new one or an existing cached one). - /// - //[MethodImpl(MethodImplOptions.AggressiveInlining)] - //public object GetOrRegisterIIdObject(object newObj, TypeMetadataWrapper wrapper) - //{ - // return ContextClass.GetOrRegisterIIdObject(newObj, wrapper); - //} - - #endregion } } diff --git a/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.BinaryDeserializationContextClass.cs b/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.BinaryDeserializationContextClass.cs index 4f57331..41604d7 100644 --- a/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.BinaryDeserializationContextClass.cs +++ b/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.BinaryDeserializationContextClass.cs @@ -1,6 +1,4 @@ using System; -using System.Collections.Generic; -using System.Runtime.CompilerServices; namespace AyCode.Core.Serializers.Binaries; @@ -28,26 +26,5 @@ public static partial class AcBinaryDeserializer Clear(); base.Reset(options); } - - /// - /// After PopulateObject, checks if we should reuse an existing IId object. - /// Uses the unified tracking from AcSerializerContextBase (IdentityMap). - /// Returns the object to use (either the new one or an existing cached one). - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public object GetOrRegisterIIdObject(object newObj, BinaryDeserializeTypeMetadata metadata) - { - if (!metadata.IsIId) return newObj; - - var wrapper = GetWrapper(newObj.GetType()); - - return metadata.IdAccessorType switch - { - IdAccessorType.Int32 => TryGetOrStoreInt32(wrapper, newObj, metadata.GetIdInt32(newObj)), - IdAccessorType.Int64 => TryGetOrStoreLong(wrapper, newObj, metadata.GetIdInt64(newObj)), - IdAccessorType.Guid => TryGetOrStoreGuid(wrapper, newObj, metadata.GetIdGuid(newObj)), - _ => newObj - }; - } } } diff --git a/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.CrossType.cs b/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.CrossType.cs index c783594..983e41a 100644 --- a/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.CrossType.cs +++ b/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.CrossType.cs @@ -214,30 +214,21 @@ public static partial class AcBinaryDeserializer /// private static object? ReadObjectWithMapping(ref BinaryDeserializationContext context, Type destType, int[] indexMapping, int depth) { + // Note: streamPosition captured before Object type code (already consumed by caller) + var streamPosition = context.Position - 1; var wrapper = context.ContextClass.GetWrapper(destType); var metadata = wrapper.Metadata; - // Handle reference ID if present - if (context.ContextClass.UseTypeReferenceHandling(metadata)) - { - var refId = context.ReadVarInt(); - if (context.ContextClass.TryGetValue(wrapper, refId, out var instance)) return instance; - - // New object with ID - will be registered below - instance = CreateInstance(destType, metadata); - if (instance != null && refId > 0) - { - context.RegisterObject(wrapper, refId, instance); - } - - PopulateObjectWithMapping(ref context, instance!, destType, indexMapping, depth); - return instance; - } - - // No reference handling + // Wire format: [Object][props...] - no refId prefix in new format var obj = CreateInstance(destType, metadata); if (obj != null) { + // Register in shared intern cache BEFORE populate (position-based sequential check) + if (context.ContextClass.UseTypeReferenceHandling(metadata)) + { + context.RegisterInternedValue(obj, streamPosition); + } + PopulateObjectWithMapping(ref context, obj, destType, indexMapping, depth); } return obj; @@ -332,18 +323,16 @@ public static partial class AcBinaryDeserializer var existingObj = propInfo.GetValue(target); if (existingObj != null) { + var objStreamPos = context.Position; // position of Object type code context.ReadByte(); // consume Object marker - - // Handle ref ID if present + + // Register in shared intern cache BEFORE populate (position-based sequential check) if (context.ContextClass.UseTypeReferenceHandling(wrapper.Metadata)) { - var refId = context.ReadVarInt(); - if (refId > 0) - { - context.RegisterObject(wrapper, refId, existingObj); - } + context.RegisterInternedValue(existingObj, objStreamPos); } - + + // Wire format: [Object][props...] - no refId prefix in new format PopulateObjectCore(ref context, existingObj, wrapper, nextDepth, skipDefaultWrite: false); return; } diff --git a/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.Populate.cs b/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.Populate.cs index 50cdd0d..d71b36c 100644 --- a/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.Populate.cs +++ b/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.Populate.cs @@ -13,19 +13,8 @@ public static partial class AcBinaryDeserializer { #region Helper Methods - /// - /// Reads hashcode prefix and registers object for Non-IId types with RefHandling=All. - /// IId types have no prefix - their Id is in the properties. - /// Call this AFTER consuming the Object marker, BEFORE PopulateObjectCore. - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void ReadAndRegisterHashcodeIfNeeded(ref BinaryDeserializationContext context, TypeMetadataWrapper wrapper, object instance) - { - if (wrapper.Metadata.IsIId || !context.ContextClass.UseTypeReferenceHandling(wrapper.Metadata)) return; - - var hashcode = context.ReadVarInt(); - context.ContextClass.TryGetOrStoreInt32(wrapper, instance, hashcode); - } + // ReadAndRegisterHashcodeIfNeeded removed - new format uses position-based footer for all reference tracking. + // No hashcode prefix in wire format anymore. #endregion @@ -39,9 +28,9 @@ public static partial class AcBinaryDeserializer /// Populate object with automatic mode detection from context. /// Uses IsMergeMode to determine merge behavior for IId collections. /// Wire format: - /// - IId types: [Object][props 0-tól...] - no refId prefix, Id is in props - /// - Non-IId + All: [Object][hashcode][props 0-tól...] - hashcode prefix - /// - Ref=Off: [Object][props 0-tól...] - no prefix + /// - IId types: [Object][props 0-t�l...] - no refId prefix, Id is in props + /// - Non-IId + All: [Object][hashcode][props 0-t�l...] - hashcode prefix + /// - Ref=Off: [Object][props 0-t�l...] - no prefix /// private static void PopulateObject(ref BinaryDeserializationContext context, object target, Type targetType, int depth) { @@ -51,19 +40,16 @@ public static partial class AcBinaryDeserializer /// /// Core populate logic shared by all populate paths. - /// Handles hashcode reading for Non-IId types internally. /// Wire format: All properties are written WITH type markers (including Id for IId types). + /// No hashcode prefix - position-based footer handles reference tracking. /// private static void PopulateObjectCore( - ref BinaryDeserializationContext context, - object target, - TypeMetadataWrapper wrapper, - int depth, + ref BinaryDeserializationContext context, + object target, + TypeMetadataWrapper wrapper, + int depth, bool skipDefaultWrite) { - // Handle hashcode for Non-IId types - ONE place for this logic! - ReadAndRegisterHashcodeIfNeeded(ref context, wrapper, target); - PopulateObjectProperties(ref context, target, wrapper.Metadata, depth, skipDefaultWrite); } diff --git a/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.cs b/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.cs index b3965cf..c5a2f98 100644 --- a/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.cs +++ b/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.cs @@ -790,7 +790,7 @@ public static partial class AcBinaryDeserializer var length = (int)context.ReadVarUInt(); if (length == 0) return string.Empty; var str = context.ReadStringUtf8(length); - context.RegisterInternedString(str, streamPosition); + context.RegisterInternedValue(str, streamPosition); return str; } @@ -922,81 +922,14 @@ public static partial class AcBinaryDeserializer #region Object Reading /// - /// Reads an ObjectRef - looks up previously registered object. - /// Wire format: - /// - IId types: [ObjectRef][Id érték] - lookup by Id - /// - Non-IId types: [ObjectRef][hashcode] - lookup by hashcode + /// Reads an ObjectRef - looks up previously registered object from shared intern cache. + /// Wire format: [ObjectRef][VarUInt cacheIndex] /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] private static object? ReadObjectRef(ref BinaryDeserializationContext context, Type targetType, int depth) { - var wrapper = context.ContextClass.GetWrapper(targetType); - return ReadObjectRef(ref context, ref wrapper); - } - - private static object? ReadObjectRef(ref BinaryDeserializationContext context, ref TypeMetadataWrapper wrapper) - { - var metadata = wrapper.Metadata; - - if (metadata.IsIId) - { - // IId: [ObjectRef][Id érték] - lookup by Id - return metadata.IdAccessorType switch - { - IdAccessorType.Int32 => ReadObjectRefInt32(ref context, ref wrapper), - IdAccessorType.Int64 => ReadObjectRefInt64(ref context, ref wrapper), - IdAccessorType.Guid => ReadObjectRefGuid(ref context, ref wrapper), - _ => throw new AcBinaryDeserializationException( - $"IId type '{metadata.SourceType.Name}' must have valid IdAccessorType", - context.Position, metadata.SourceType) - }; - } - else - { - // Non-IId: [ObjectRef][hashcode] - lookup by hashcode (always Int32) - var hashcode = context.ReadVarInt(); - if (wrapper.TryGetValueInt32(hashcode, out var instance)) - return instance; - - throw new AcBinaryDeserializationException( - $"ObjectRef hashcode {hashcode} not found for type '{metadata.SourceType.Name}'", - context.Position, metadata.SourceType); - } - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static object? ReadObjectRefInt32(ref BinaryDeserializationContext context, ref TypeMetadataWrapper wrapper) - { - var id = context.ReadVarInt(); - if (wrapper.TryGetValueInt32(id, out var instance)) - return instance; - - throw new AcBinaryDeserializationException( - $"ObjectRef Id {id} not found for type '{wrapper.Metadata.SourceType.Name}'", - context.Position, wrapper.Metadata.SourceType); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static object? ReadObjectRefInt64(ref BinaryDeserializationContext context, ref TypeMetadataWrapper wrapper) - { - var id = context.ReadVarLong(); - if (wrapper.TryGetValueInt64(id, out var instance)) - return instance; - - throw new AcBinaryDeserializationException( - $"ObjectRef Id {id} not found for type '{wrapper.Metadata.SourceType.Name}'", - context.Position, wrapper.Metadata.SourceType); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static object? ReadObjectRefGuid(ref BinaryDeserializationContext context, ref TypeMetadataWrapper wrapper) - { - var id = context.ReadGuidUnsafe(); - if (wrapper.TryGetValueGuid(id, out var instance)) - return instance; - - throw new AcBinaryDeserializationException( - $"ObjectRef Id {id} not found for type '{wrapper.Metadata.SourceType.Name}'", - context.Position, wrapper.Metadata.SourceType); + var cacheIndex = (int)context.ReadVarUInt(); + return context.GetInternedObject(cacheIndex); } /// @@ -1008,60 +941,34 @@ public static partial class AcBinaryDeserializer /// private static object? ReadObject(ref BinaryDeserializationContext context, Type targetType, int depth) { + // Capture stream position of the Object type code (already consumed) + var streamPosition = context.Position - 1; + // Handle dictionary types if (IsDictionaryType(targetType, out var keyType, out var valueType)) { return ReadDictionaryAsObject(ref context, keyType!, valueType!, depth); } - + var wrapper = context.ContextClass.GetWrapper(targetType); var metadata = wrapper.Metadata; - object? instance; + // Wire format: [Object][props...] - no hashcode prefix, no Id prefix + // Position-based footer handles all reference tracking + var instance = CreateInstance(targetType, metadata); + if (instance == null) return null; + // Register in shared intern cache BEFORE populate (position-based sequential check) + // Instance is populated in-place, so registering early is safe. + // Must happen before reading inner content (strings/objects) that may also need registration. if (context.ContextClass.UseTypeReferenceHandling(metadata)) { - if (metadata.IsIId) - { - // IId: [Object][props 0-tól...] - // Create → Populate (Id beolvasódik) → Register by Id - instance = CreateInstance(targetType, metadata); - if (instance == null) return null; - - PopulateObject(ref context, instance, metadata, depth, skipDefaultWrite: true); - - // Register by Id after populate - uses typed methods on wrapper - RegisterIIdInstance(context.ContextClass, wrapper, instance, metadata); - } - else - { - // Non-IId + All: [Object][hashcode][props 0-tól...] - var hashcode = context.ReadVarInt(); - - // TryGetValue - if already exists, return (shouldn't happen for Object, only ObjectRef) - if (wrapper.TryGetValueInt32(hashcode, out instance)) - return instance; - - // Create + Register by hashcode (always Int32 for Non-IId) - instance = CreateInstance(targetType, metadata); - if (instance == null) return null; - wrapper.TryGetOrStoreInt32(hashcode, instance); - - // Populate - PopulateObject(ref context, instance, metadata, depth, skipDefaultWrite: true); - } + context.RegisterInternedValue(instance, streamPosition); } - else - { - // Ref=Off: [Object][props 0-tól...] - instance = CreateInstance(targetType, metadata); - if (instance == null) return null; - - PopulateObject(ref context, instance, metadata, depth, skipDefaultWrite: true); - } - + + PopulateObject(ref context, instance, metadata, depth, skipDefaultWrite: true); + // ChainMode: Register/retrieve from chain tracker (separate from reference handling) - // Note: For ChainMode, we need IdGetter OR typed getters (IdAccessorType != None) if (context.IsChainMode && metadata.IsIId && metadata.IdType != null) { var id = GetIdBoxed(instance, metadata); @@ -1074,12 +981,12 @@ public static partial class AcBinaryDeserializer CopyProperties(instance, existingObj!, metadata); return existingObj; } - + // Register this new object context.ChainTracker.TryRegisterIIdObject(instance); } } - + return instance; } @@ -1424,7 +1331,7 @@ public static partial class AcBinaryDeserializer var byteLen = (int)context.ReadVarUInt(); if (byteLen == 0) return; var str = context.ReadStringUtf8(byteLen); - context.RegisterInternedString(str, streamPosition); + context.RegisterInternedValue(str, streamPosition); } ///// @@ -1447,18 +1354,8 @@ public static partial class AcBinaryDeserializer private static void SkipObject(ref BinaryDeserializationContext context, BinaryDeserializeTypeMetadata metaData) { - // Wire format: - // - IId: [Object][props 0-tól...] - no hashcode - // - Non-IId + All: [Object][hashcode][props 0-tól...] - hashcode present - if (context.ContextClass.UseTypeReferenceHandling(metaData) && !metaData.IsIId) - { - // Non-IId: skip hashcode - context.ReadVarInt(); - } - + // Wire format: [Object][props...] - no hashcode prefix in new format // NEW FORMAT: Can't skip without knowing property count! - // Need to read type metadata to know how many properties to skip - // For now, throw exception - SkipObject not supported with new format throw new NotSupportedException( "SkipObject is not supported with SKIP marker format. " + "Unable to determine property count without type metadata."); @@ -1486,21 +1383,7 @@ 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). /// diff --git a/AyCode.Core/Serializers/Binaries/AcBinarySerializer.BinarySerializationContext.cs b/AyCode.Core/Serializers/Binaries/AcBinarySerializer.BinarySerializationContext.cs index 212c74c..3a4e771 100644 --- a/AyCode.Core/Serializers/Binaries/AcBinarySerializer.BinarySerializationContext.cs +++ b/AyCode.Core/Serializers/Binaries/AcBinarySerializer.BinarySerializationContext.cs @@ -122,6 +122,10 @@ public static partial class AcBinarySerializer // These properties delegate to Options for convenience public bool UseStringInterning => Options.UseStringInterning != StringInterningMode.None; + /// + /// True if we need footer position in header (string interning OR reference handling). + /// + public bool HasFooter => UseStringInterning || ReferenceHandling != ReferenceHandlingMode.None; public bool UseMetadata => Options.UseMetadata; public byte MinStringInternLength => Options.MinStringInternLength; public byte MaxStringInternLength => Options.MaxStringInternLength; @@ -263,41 +267,99 @@ public static partial class AcBinarySerializer public int GetDupCount() => _nextCacheIndex; /// - /// Writes the footer with (position, cacheIndex) pairs sorted by position. + /// Writes the merged footer with (position, cacheIndex) pairs sorted by position. + /// Collects entries from string interning AND ID tracking (all wrappers). /// VarUInt format for compact size, deserializer reads into flat int[]. /// - public void WriteInternedStringFooter() + public void WriteInternedFooter() { - if (_stringInternMap == null || _nextCacheIndex == 0) return; + if (_nextCacheIndex == 0) return; - // Collect entries with CacheIndex >= 0 (occurred more than once) - // We need to sort by StreamPosition for deserializer sequential access + // Collect ALL entries with CacheIndex >= 0 (string + ID, all occurred more than once) Span<(int Position, int CacheIndex)> entries = _nextCacheIndex <= 64 ? stackalloc (int, int)[_nextCacheIndex] : new (int, int)[_nextCacheIndex]; var idx = 0; - var count = _stringInternMap.Count; - for (var i = 0; i < count; i++) + + // 1. String intern entries + if (_stringInternMap != null) { - ref var entry = ref _stringInternMap.GetValueRefAt(i); - if (entry.CacheIndex >= 0) + var count = _stringInternMap.Count; + for (var i = 0; i < count; i++) { - entries[idx++] = (entry.StreamPosition, entry.CacheIndex); + ref var entry = ref _stringInternMap.GetValueRefAt(i); + if (entry.CacheIndex >= 0) + entries[idx++] = (entry.StreamPosition, entry.CacheIndex); } } + // 2. ID tracking entries from all wrappers + foreach (var wrapper in GetWrappers()) + { + CollectInternEntries(wrapper.IdentityMapInt32, ref entries, ref idx); + CollectInternEntries(wrapper.IdentityMapInt64, ref entries, ref idx); + CollectInternEntries(wrapper.IdentityMapGuid, ref entries, ref idx); + } + // Sort by StreamPosition (ascending) for deserializer sequential check - entries.Sort((a, b) => a.Position.CompareTo(b.Position)); + var usedEntries = entries.Slice(0, idx); + usedEntries.Sort((a, b) => a.Position.CompareTo(b.Position)); // Write pairs as VarUInt for compact size - for (var i = 0; i < _nextCacheIndex; i++) + for (var i = 0; i < idx; i++) { - WriteVarUInt((uint)entries[i].Position); - WriteVarUInt((uint)entries[i].CacheIndex); + WriteVarUInt((uint)usedEntries[i].Position); + WriteVarUInt((uint)usedEntries[i].CacheIndex); } } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void CollectInternEntries(IdentityMap? map, + ref Span<(int Position, int CacheIndex)> 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 >= 0) + entries[idx++] = (entry.StreamPosition, entry.CacheIndex); + } + } + + #endregion + + #region Object Reference Tracking (IId + Non-IId) + + /// + /// Tries to track an IId object (Int32 Id). Uses shared _nextCacheIndex with string interning. + /// Returns true if first occurrence, false if already seen (cacheIndex assigned). + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool TryTrackObject(TypeMetadataWrapper wrapper, object obj, out int cacheIndex) + { + return TryTrack(wrapper, obj, _position, ref _nextCacheIndex, 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, _position, ref _nextCacheIndex, 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, _position, ref _nextCacheIndex, out cacheIndex); + } + #endregion #region Property Name Table @@ -971,10 +1033,10 @@ public static partial class AcBinarySerializer // Header layout: // [0] version (1 byte) // [1] flags (1 byte) - // [2-5] footer position (4 bytes, only if UseStringInterning) - EnsureCapacity(UseStringInterning ? 6 : 2); + // [2-5] footer position (4 bytes, if string interning OR reference handling) + EnsureCapacity(HasFooter ? 6 : 2); _headerPosition = _position; - _position += UseStringInterning ? 6 : 2; + _position += HasFooter ? 6 : 2; } /// @@ -991,7 +1053,7 @@ public static partial class AcBinarySerializer public void FinalizeHeaderSections() { var hasPropertyNames = UseMetadata && _propertyNameList is { Count: > 0 }; - var dupCount = UseStringInterning ? GetDupCount() : 0; + var dupCount = GetDupCount(); // Shared counter: string intern + ID tracking var hasInternTable = dupCount > 0; // Calculate property names header size (strings go to footer now) @@ -1008,7 +1070,7 @@ public static partial class AcBinarySerializer } // Write property names to header if needed - var headerPayloadStart = _headerPosition + (UseStringInterning ? 6 : 2); + var headerPayloadStart = _headerPosition + (HasFooter ? 6 : 2); if (hasPropertyNames) { var headerPos = headerPayloadStart; @@ -1020,12 +1082,13 @@ public static partial class AcBinarySerializer } } - // Footer: write indices of strings that occurred more than once + // Footer: write merged intern entries (string + ID) var footerPosition = 0; if (hasInternTable) { footerPosition = _position; - WriteFooterStringIndices(dupCount); + WriteVarUInt((uint)dupCount); + WriteInternedFooter(); } // Write header @@ -1037,36 +1100,21 @@ public static partial class AcBinarySerializer flags |= BinaryTypeCode.HeaderFlag_RefHandling_OnlyId; else if (ReferenceHandling == ReferenceHandlingMode.All) flags |= (byte)(BinaryTypeCode.HeaderFlag_RefHandling_OnlyId | BinaryTypeCode.HeaderFlag_RefHandling_All); - // Set footer position flag if string interning is enabled - if (UseStringInterning) + // Set footer position flag if footer is needed (string interning OR ref handling) + if (HasFooter) flags |= BinaryTypeCode.HeaderFlag_HasFooterPosition; _buffer[_headerPosition] = AcBinarySerializerOptions.FormatVersion; _buffer[_headerPosition + 1] = flags; - // Always write footer position if string interning is enabled in options + // Write footer position if footer is needed // (even if there's no actual interned data - footer position will be 0) - if (UseStringInterning) + if (HasFooter) { Unsafe.WriteUnaligned(ref _buffer[_headerPosition + 2], footerPosition); } } - /// - /// Writes the footer with total count (for verification) + dup count + indices. - /// Footer format: [totalStringCount][dupCount][dupIndex0][dupIndex1]... - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - /// - /// Writes footer: [dupCount][(position, cacheIndex), ...] - /// Position-based format for 100% reliable deserializer matching. - /// - private void WriteFooterStringIndices(int dupCount) - { - // Dup count + (position, cacheIndex) pairs - WriteVarUInt((uint)dupCount); - WriteInternedStringFooter(); - } /// /// Writes UTF8 string at specific position, optimized for ASCII strings. diff --git a/AyCode.Core/Serializers/Binaries/AcBinarySerializer.cs b/AyCode.Core/Serializers/Binaries/AcBinarySerializer.cs index 238411f..dce7683 100644 --- a/AyCode.Core/Serializers/Binaries/AcBinarySerializer.cs +++ b/AyCode.Core/Serializers/Binaries/AcBinarySerializer.cs @@ -846,36 +846,36 @@ public static partial class AcBinarySerializer { if (metadata.IsIId) { - // IId típus: track by Id, ObjectRef writes Id + // IId típus: track by Id, ObjectRef writes cacheIndex switch (metadata.IdAccessorType) { case IdAccessorType.Int32: - if (!context.TryTrack(wrapper, value, out int intId)) + if (!context.TryTrackObject(wrapper, value, out int cacheIndex32)) { - // Already seen → ObjectRef + Id + // Already seen → ObjectRef + cacheIndex context.WriteByte(BinaryTypeCode.ObjectRef); - context.WriteVarInt(intId); + context.WriteVarUInt((uint)cacheIndex32); return; } // First occurrence → Object (no extra data, Id in props) context.WriteByte(BinaryTypeCode.Object); break; - + case IdAccessorType.Int64: - if (!context.TryTrack(wrapper, value, out long longId)) + if (!context.TryTrackObjectLong(wrapper, value, out int cacheIndex64)) { context.WriteByte(BinaryTypeCode.ObjectRef); - context.WriteVarLong(longId); + context.WriteVarUInt((uint)cacheIndex64); return; } context.WriteByte(BinaryTypeCode.Object); break; - + case IdAccessorType.Guid: - if (!context.TryTrack(wrapper, value, out Guid guidId)) + if (!context.TryTrackObjectGuid(wrapper, value, out int cacheIndexGuid)) { context.WriteByte(BinaryTypeCode.ObjectRef); - context.WriteGuidBits(guidId); + context.WriteVarUInt((uint)cacheIndexGuid); return; } context.WriteByte(BinaryTypeCode.Object); @@ -884,17 +884,16 @@ public static partial class AcBinarySerializer } else { - // Non-IId + RefHandling=All: track by hashcode (IdAccessorType=Int32, RefIdGetter=GetHashCode) - if (!context.TryTrack(wrapper, value, out int hashcode)) + // Non-IId + RefHandling=All: track by hashcode + if (!context.TryTrackObject(wrapper, value, out int cacheIndexHash)) { - // Already seen → ObjectRef + hashcode + // Already seen → ObjectRef + cacheIndex context.WriteByte(BinaryTypeCode.ObjectRef); - context.WriteVarInt(hashcode); + context.WriteVarUInt((uint)cacheIndexHash); return; } - // First occurrence → Object + hashcode + props + // First occurrence → Object (no extra prefix for Non-IId in new format) context.WriteByte(BinaryTypeCode.Object); - context.WriteVarInt(hashcode); } } else diff --git a/AyCode.Core/Serializers/IdentityMap.cs b/AyCode.Core/Serializers/IdentityMap.cs index 9108c01..682774a 100644 --- a/AyCode.Core/Serializers/IdentityMap.cs +++ b/AyCode.Core/Serializers/IdentityMap.cs @@ -59,16 +59,6 @@ public interface IIdentityMap /// The value type public sealed class IdentityMap : IIdentityMap where TKey : notnull { - private const bool _useSmallInt = false; - - // Small int optimization (TKey = int only, 0-4095), 512 bytes (fits in L1 cache!) - private const int SmallBitmapSize = 64; - private const int SmallSize = 4096; - - // Small int optimization (TKey = int only, 0-65535) - //private const int SmallBitmapSize = 1024; - //private const int SmallSize = SmallBitmapSize * 64; - // Slot for hash table entries (generation needed for hash table validity) private struct HashSlot { @@ -76,10 +66,6 @@ public sealed class IdentityMap : IIdentityMap where TKey : notnul public int Next; // next slot index in chain (-1 = end) } - // Small int storage - SIMPLE: bitmap for "seen?" + direct TValue[] for values - // Bitmap protects values - no generation needed, no value array clearing on Reset! - private ulong[]? _smallBitmap; // Cache-friendly "seen?" check (512 bytes, cleared on Reset) - // Hash table storage (for large ints and other types) private int[]? _buckets; // bucket index → first entry index private HashSlot[]? _entries; // hash table entries @@ -97,32 +83,15 @@ public sealed class IdentityMap : IIdentityMap where TKey : notnul private static readonly bool IsString = typeof(TKey) == typeof(string); private static readonly bool IsValueTypeValue = typeof(TValue).IsValueType; + /// + /// Number of entries in the hash table. Use with GetValueRefAt for iteration. + /// + public int Count => _count; + public IdentityMap() { } - /// - /// Tries to add a key to tracking (serialization). - /// Returns true if first occurrence (key was added). - /// Returns false if already seen. - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public bool TryAddKey(TKey key) - { - // Small int fast path - if (_useSmallInt && IsInt32) - { - var intKey = Unsafe.As(ref key); - if ((uint)intKey < SmallSize) - { - return TryAddSmallInt(intKey); - } - } - - // Hash table path - 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). @@ -135,32 +104,6 @@ public sealed class IdentityMap : IIdentityMap where TKey : notnul 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) - { - // Lazy init - ONLY bitmap for tracking - if (_smallBitmap == null) - { - _smallBitmap = ArrayPool.Shared.Rent(SmallBitmapSize); - Array.Clear(_smallBitmap, 0, SmallBitmapSize); - } - - var wordIdx = key >> 6; - var bit = 1UL << (key & 63); - - ref var word = ref _smallBitmap[wordIdx]; - if ((word & bit) != 0) - return false; // Already seen (bitmap is cleared on Reset) - - word |= bit; - return true; - } - [MethodImpl(MethodImplOptions.AggressiveInlining)] private static bool KeyEquals(TKey a, TKey b) { @@ -297,96 +240,6 @@ public sealed class IdentityMap : IIdentityMap where TKey : notnul _keys = newKeys; } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public bool HasKey(TKey key) - { - // Small int fast path - bitmap check is cache-friendly (512 bytes) - if (_useSmallInt && IsInt32) - { - var intKey = Unsafe.As(ref key); - if ((uint)intKey < SmallSize && _smallBitmap != null) - { - var wordIdx = intKey >> 6; - var bit = 1UL << (intKey & 63); - return (_smallBitmap[wordIdx] & bit) != 0; - } - } - - // Hash table path - if (_buckets == null) return false; - - var hash = GetHashCode(key); - var bucketIdx = (hash & 0x7FFFFFFF) % _bucketsLength; - - for (var i = _buckets[bucketIdx]; i >= 0; i = _entries![i].Next) - { - if (KeyEquals(_keys![i], key)) - return true; - } - return false; - } - - /// - /// Checks if key exists and returns existing value, or adds new key (deserialization). - /// Returns true if first occurrence (key was added, out = default). - /// Returns false if already seen (out = existing value). - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public bool TryGetOrAddKey(TKey key, out TValue? existing) - { - if (!TryAddHash(key, out var slotIndex)) - { - existing = _entries![slotIndex].Value; - return false; - } - existing = default; - return true; - } - - /// - /// Gets existing value or adds new value for key (deserialization with value storage). - /// Returns the existing value if key was seen before, or stores and returns newValue. - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public TValue TryGetOrAddValue(TKey key, TValue newValue) - { - if (!TryAddHash(key, out var slotIndex)) - { - var existing = _entries![slotIndex].Value; - if (existing != null) return existing; - } - _entries![slotIndex].Value = newValue; - return newValue; - } - - /// - /// Tries to get the value for a key. - /// Returns true if found, false if not. - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public bool TryGetValue(TKey key, out TValue? value) - { - if (_buckets == null) - { - value = default; - return false; - } - - var hash = GetHashCode(key); - var bucketIdx = (hash & 0x7FFFFFFF) % _bucketsLength; - - for (var i = _buckets[bucketIdx]; i >= 0; i = _entries![i].Next) - { - if (KeyEquals(_keys![i], key)) - { - value = _entries[i].Value; - return true; - } - } - value = default; - return false; - } - /// /// Returns a reference to the value at the given slot index. /// Use with slotIndex from TryAdd for in-place value modification. @@ -415,68 +268,60 @@ public sealed class IdentityMap : IIdentityMap where TKey : notnul /// If true, pre-rent arrays at next capacity (useful for async Clear to shift work from hot path) public void Reset(bool preRentBuckets = false) { - if (_smallBitmap != null) + if (_buckets == null) return; + + // Small arrays: keep and clear (faster than pool round-trip) + if (_bucketsLength <= InitialHashCapacity * 5) { - Array.Clear(_smallBitmap, 0, SmallBitmapSize); - //ArrayPool.Shared.Return(_smallBitmap); - //_smallBitmap = null; - } - - if (_buckets != null) - { - // Small arrays: keep and clear (faster than pool round-trip) - if (_bucketsLength <= InitialHashCapacity * 5) - { - Array.Fill(_buckets, -1, 0, _bucketsLength); - // Clear to release object references (GC can collect) - if (_count > 0) - { - Array.Clear(_entries!, 0, _count); - Array.Clear(_keys!, 0, _count); - } - _count = 0; - return; - } - - // Large arrays: return to pool, remember half capacity - var nextCapacity = Math.Max(_bucketsLength / 2, InitialHashCapacity * 5); - - // Clear entries/keys to release object references before returning to pool - // Otherwise pool holds refs → GC can't collect! + Array.Fill(_buckets, -1, 0, _bucketsLength); + // Clear to release object references (GC can collect) if (_count > 0) { Array.Clear(_entries!, 0, _count); Array.Clear(_keys!, 0, _count); } - - ArrayPool.Shared.Return(_buckets); - ArrayPool.Shared.Return(_entries!,false); - ArrayPool.Shared.Return(_keys!, false); - - if (preRentBuckets) - { - // Pre-rent arrays now (async background) so Pool.Get() is faster - _buckets = ArrayPool.Shared.Rent(nextCapacity); - _bucketsLength = _buckets.Length; - Array.Fill(_buckets, -1, 0, _bucketsLength); - - _entries = ArrayPool.Shared.Rent(nextCapacity); - _entriesLength = _entries.Length; - //Array.Clear(_entries, 0, _entriesLength); - - _keys = ArrayPool.Shared.Rent(nextCapacity); - //Array.Clear(_keys, 0, nextCapacity); - } - else - { - _buckets = null; - _entries = null; - _keys = null; - _bucketsLength = nextCapacity; // Remember for next InitHashTable - _entriesLength = 0; - } _count = 0; + return; } + + // Large arrays: return to pool, remember half capacity + var nextCapacity = Math.Max(_bucketsLength / 2, InitialHashCapacity * 5); + + // Clear entries/keys to release object references before returning to pool + // Otherwise pool holds refs → GC can't collect! + if (_count > 0) + { + Array.Clear(_entries!, 0, _count); + Array.Clear(_keys!, 0, _count); + } + + ArrayPool.Shared.Return(_buckets); + ArrayPool.Shared.Return(_entries!,false); + ArrayPool.Shared.Return(_keys!, false); + + if (preRentBuckets) + { + // Pre-rent arrays now (async background) so Pool.Get() is faster + _buckets = ArrayPool.Shared.Rent(nextCapacity); + _bucketsLength = _buckets.Length; + Array.Fill(_buckets, -1, 0, _bucketsLength); + + _entries = ArrayPool.Shared.Rent(nextCapacity); + _entriesLength = _entries.Length; + //Array.Clear(_entries, 0, _entriesLength); + + _keys = ArrayPool.Shared.Rent(nextCapacity); + //Array.Clear(_keys, 0, nextCapacity); + } + else + { + _buckets = null; + _entries = null; + _keys = null; + _bucketsLength = nextCapacity; // Remember for next InitHashTable + _entriesLength = 0; + } + _count = 0; } } diff --git a/AyCode.Core/Serializers/SerializationContextBase.cs b/AyCode.Core/Serializers/SerializationContextBase.cs index e8e24a4..b96360b 100644 --- a/AyCode.Core/Serializers/SerializationContextBase.cs +++ b/AyCode.Core/Serializers/SerializationContextBase.cs @@ -15,94 +15,42 @@ public abstract class SerializationContextBase : AcSerializ where TMetadata : TypeMetadataBase where TOptions : AcSerializerOptions { - private bool _useSmallInt = false; - private const int BitArraySize = 1024; - private const int MaxSmallId = BitArraySize * 64; - #region Id Extraction (to be implemented by derived sealed classes) - - // Future: Abstract methods for Id extraction - // protected abstract int GetIdInt32Core(object obj, TMetadata metadata); - // protected abstract long GetIdInt64Core(object obj, TMetadata metadata); - // protected abstract Guid GetIdGuidCore(object obj, TMetadata metadata); - - #endregion - - #region Tracking (to be moved from AcSerializerContextBase) + #region Tracking - InternEntry based (serializer side) /// /// Tries to track an object with int RefId. - /// Use when wrapper.Metadata.IdAccessorType == Int32. - /// Returns true if first occurrence. + /// Returns true if first occurrence, false if already seen (cacheIndex assigned). + /// Uses shared nextCacheIndex counter (shared with string interning). /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public bool TryTrack(TypeMetadataWrapper wrapper, object obj, out int refId) + public bool TryTrack(TypeMetadataWrapper wrapper, object obj, int streamPosition, ref int nextCacheIndex, out int cacheIndex) { Debug.Assert(wrapper.Metadata.IdAccessorType == IdAccessorType.Int32); - - // Use pre-cast getter - no cast overhead! - refId = wrapper.RefIdGetterInt32!(obj); - - // BitArray fast path for small positive IDs - return (_useSmallInt && refId is >= 0 and < MaxSmallId) ? TryTrackSmallId(wrapper, refId) : wrapper.TryAddKey(refId); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static bool TryTrackSmallId(TypeMetadataWrapper wrapper, int id) - { - // Lazy init - but only once per type per serialization - var bitmap = wrapper.SmallIdBitmap ?? InitSmallIdBitmap(wrapper); - - var idx = (uint)id; - var wordIdx = idx >> 6; - var bitIdx = (int)(idx & 63); - - var mask = 1UL << bitIdx; - ref var word = ref bitmap[wordIdx]; - - if ((word & mask) != 0) return false; - - word |= mask; - return true; - } - - [MethodImpl(MethodImplOptions.NoInlining)] - private static ulong[] InitSmallIdBitmap(TypeMetadataWrapper wrapper) - { - wrapper.SmallIdBitmap = new ulong[BitArraySize]; - return wrapper.SmallIdBitmap; + var id = wrapper.RefIdGetterInt32!(obj); + return wrapper.TryTrackInt32(id, streamPosition, ref nextCacheIndex, out cacheIndex); } /// /// Tries to track an object with long RefId. - /// Use when wrapper.Metadata.IdAccessorType == Int64. - /// Returns true if first occurrence. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public bool TryTrack(TypeMetadataWrapper wrapper, object obj, out long refId) + public bool TryTrackLong(TypeMetadataWrapper wrapper, object obj, int streamPosition, ref int nextCacheIndex, out int cacheIndex) { Debug.Assert(wrapper.Metadata.IdAccessorType == IdAccessorType.Int64); - - // Use pre-cast getter - no cast overhead! - refId = wrapper.RefIdGetterInt64!(obj); - - return wrapper.TryAddKey(refId); + var id = wrapper.RefIdGetterInt64!(obj); + return wrapper.TryTrackInt64(id, streamPosition, ref nextCacheIndex, out cacheIndex); } /// /// Tries to track an object with Guid RefId. - /// Use when wrapper.Metadata.IdAccessorType == Guid. - /// Returns true if first occurrence. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public bool TryTrack(TypeMetadataWrapper wrapper, object obj, out Guid refId) + public bool TryTrackGuid(TypeMetadataWrapper wrapper, object obj, int streamPosition, ref int nextCacheIndex, out int cacheIndex) { Debug.Assert(wrapper.Metadata.IdAccessorType == IdAccessorType.Guid); - - // Use pre-cast getter - no cast overhead! - refId = wrapper.RefIdGetterGuid!(obj); - - return wrapper.TryAddKey(refId); + var id = wrapper.RefIdGetterGuid!(obj); + return wrapper.TryTrackGuid(id, streamPosition, ref nextCacheIndex, out cacheIndex); } #endregion diff --git a/AyCode.Core/Serializers/TypeMetadataWrapper.cs b/AyCode.Core/Serializers/TypeMetadataWrapper.cs index 65153f9..bd734ec 100644 --- a/AyCode.Core/Serializers/TypeMetadataWrapper.cs +++ b/AyCode.Core/Serializers/TypeMetadataWrapper.cs @@ -36,21 +36,21 @@ public sealed class TypeMetadataWrapper where TMetadata : TypeMetadat 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 IdentityMap? IdentityMapInt32; - + internal IdentityMap? IdentityMapInt32; + /// /// Typed IdentityMap for Int64 IDs. Direct access, no type check. /// - internal IdentityMap? IdentityMapInt64; - + internal IdentityMap? IdentityMapInt64; + /// /// Typed IdentityMap for Guid IDs. Direct access, no type check. /// - internal IdentityMap? IdentityMapGuid; + internal IdentityMap? IdentityMapGuid; #endregion @@ -68,11 +68,6 @@ public sealed class TypeMetadataWrapper where TMetadata : TypeMetadat /// 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. @@ -85,21 +80,18 @@ public sealed class TypeMetadataWrapper where TMetadata : TypeMetadat // Use cached delegate from metadata for IId types, static fallback for non-IId var refIdGetter = metadata.TypedIdGetter ?? HashCodeGetter; - - // Pre-cast typed getters AND set RegisterById delegate - avoids switch in every call + + // Pre-cast typed getters based on IdAccessorType switch (metadata.IdAccessorType) { case IdAccessorType.Int32: RefIdGetterInt32 = (Func)refIdGetter; - RegisterById = RegisterByInt32Id; // Method group - no allocation! break; case IdAccessorType.Int64: RefIdGetterInt64 = (Func)refIdGetter; - RegisterById = RegisterByInt64Id; break; case IdAccessorType.Guid: RefIdGetterGuid = (Func)refIdGetter; - RegisterById = RegisterByGuidId; break; } } @@ -126,166 +118,85 @@ public sealed class TypeMetadataWrapper where TMetadata : TypeMetadat IdentityMapGuid?.Reset(preRentBuckets); } - #region Direct Int32 Operations - No type check, no generic overhead - - /// - /// Tries to get object by Int32 Id. Returns true if found. - /// Direct dictionary lookup - no generic type check! - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public bool TryGetValueInt32(int id, out object? instance) - { - 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 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 IdentityMap(); - map.TryGetOrAddValue(id, instance); - } - - #endregion + #region Serializer: TryTrack by typed Id → InternEntry with slotIndex - #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 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 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 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 IdentityMap(); - map.TryGetOrAddValue(id, instance); - } - - #endregion - - #region Legacy Generic Methods (for backward compatibility) - /// - /// Gets or creates the typed IdentityMap for tracking. - /// SLOW PATH - use typed methods (TryGetValueInt32, etc.) instead! + /// Tries to track Int32 Id. Returns true if first occurrence. + /// On repeat: assigns CacheIndex from shared counter, returns cacheIndex. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public IdentityMap GetOrCreateIdentityMap() where TId : notnull + public bool TryTrackInt32(int id, int streamPosition, ref int nextCacheIndex, out int cacheIndex) { - // Route to typed fields based on TId - if (typeof(TId) == typeof(int)) + if (id == 0) { cacheIndex = -1; return true; } // Default Id - no tracking + + var map = IdentityMapInt32 ??= new IdentityMap(); + if (!map.TryAdd(id, out var slotIndex)) { - var map = IdentityMapInt32 ??= new IdentityMap(); - return Unsafe.As, IdentityMap>(ref map); - } - if (typeof(TId) == typeof(long)) - { - var map = IdentityMapInt64 ??= new IdentityMap(); - return Unsafe.As, IdentityMap>(ref map); - } - if (typeof(TId) == typeof(Guid)) - { - var map = IdentityMapGuid ??= new IdentityMap(); - return Unsafe.As, IdentityMap>(ref map); + // 2+ occurrence: assign CacheIndex if first repeat + ref var entry = ref map.GetValueRef(slotIndex); + if (entry.CacheIndex < 0) + entry.CacheIndex = nextCacheIndex++; + cacheIndex = entry.CacheIndex; + return false; } - throw new NotSupportedException($"Id type {typeof(TId)} is not supported"); + // 1st occurrence: store stream position + ref var newEntry = ref map.GetValueRef(slotIndex); + newEntry.StreamPosition = streamPosition; + newEntry.CacheIndex = -1; + cacheIndex = -1; + return true; } + /// + /// Tries to track Int64 Id. Returns true if first occurrence. + /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public bool TryAddKey(TId key) where TId : struct + public bool TryTrackInt64(long id, int streamPosition, ref int nextCacheIndex, out int cacheIndex) { - var map = GetOrCreateIdentityMap(); - return map.TryAddKey(key); + if (id == 0) { cacheIndex = -1; return true; } + + var map = IdentityMapInt64 ??= new IdentityMap(); + if (!map.TryAdd(id, out var slotIndex)) + { + ref var entry = ref map.GetValueRef(slotIndex); + if (entry.CacheIndex < 0) + entry.CacheIndex = nextCacheIndex++; + cacheIndex = entry.CacheIndex; + return false; + } + + ref var newEntry = ref map.GetValueRef(slotIndex); + newEntry.StreamPosition = streamPosition; + newEntry.CacheIndex = -1; + cacheIndex = -1; + return true; } + /// + /// Tries to track Guid Id. Returns true if first occurrence. + /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public bool TryGetValue(TId refId, out object? instance) where TId : struct + public bool TryTrackGuid(Guid id, int streamPosition, ref int nextCacheIndex, out int cacheIndex) { - var map = GetOrCreateIdentityMap(); - return map.TryGetValue(refId, out instance); + if (id == Guid.Empty) { cacheIndex = -1; return true; } + + var map = IdentityMapGuid ??= new IdentityMap(); + if (!map.TryAdd(id, out var slotIndex)) + { + ref var entry = ref map.GetValueRef(slotIndex); + if (entry.CacheIndex < 0) + entry.CacheIndex = nextCacheIndex++; + cacheIndex = entry.CacheIndex; + return false; + } + + ref var newEntry = ref map.GetValueRef(slotIndex); + newEntry.StreamPosition = streamPosition; + newEntry.CacheIndex = -1; + cacheIndex = -1; + return true; } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public object TryGetOrStoreId(TId id, object newObj) where TId : struct - { - var map = GetOrCreateIdentityMap(); - return map.TryGetOrAddValue(id, newObj); - } - #endregion }