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.
This commit is contained in:
Loretta 2026-02-01 12:18:27 +01:00
parent 23af1fc98b
commit 9b151fd6cf
11 changed files with 325 additions and 811 deletions

View File

@ -83,54 +83,13 @@ public abstract class AcSerializerContextBase<TMetadata, TOptions>
#endregion #endregion
#region Tracking API - int #region Wrapper Iteration (for footer writing)
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool TryGetValue(TypeMetadataWrapper<TMetadata> wrapper, int refId, out object? instance)
{
return wrapper.TryGetValue(refId, out instance);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool TryGetValue(TypeMetadataWrapper<TMetadata> wrapper, long refId, out object? instance)
{
return wrapper.TryGetValue(refId, out instance);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool TryGetValue(TypeMetadataWrapper<TMetadata> wrapper, Guid refId, out object? instance)
{
return wrapper.TryGetValue(refId, out instance);
}
/// <summary> /// <summary>
/// For deserialization: checks if an object with this Id was already seen. /// Iterates all wrappers and collects InternEntry data for ID tracking footer.
/// If yes, returns the existing object. If no, stores this object and returns it.
/// Uses IdentityMap.TryGetOrAddValue - single Dictionary operation!
/// </summary> /// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public object TryGetOrStoreInt32(TypeMetadataWrapper<TMetadata> wrapper, object newObj, int id) public Dictionary<Type, TypeMetadataWrapper<TMetadata>>.ValueCollection GetWrappers() => _wrappers.Values;
{
return id == 0 ? newObj : wrapper.TryGetOrStoreId(id, newObj);
}
/// <summary>
/// For deserialization: checks if an object with this Id was already seen (long version).
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public object TryGetOrStoreLong(TypeMetadataWrapper<TMetadata> wrapper, object newObj, long id)
{
return id == 0 ? newObj : wrapper.TryGetOrStoreId(id, newObj);
}
/// <summary>
/// For deserialization: checks if an object with this Id was already seen (Guid version).
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public object TryGetOrStoreGuid(TypeMetadataWrapper<TMetadata> wrapper, object newObj, Guid id)
{
return id == Guid.Empty ? newObj : wrapper.TryGetOrStoreId(id, newObj);
}
#endregion #endregion

View File

@ -20,10 +20,11 @@ public static partial class AcBinaryDeserializer
private List<string>? _propertyNames; private List<string>? _propertyNames;
private Dictionary<int, string>? _stringCache; private Dictionary<int, string>? _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 // 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 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 _dupCheckIndex; // Current index in _dupData (increments by 2)
private int _nextDupPosition; // Cached next dup position - avoids array access in hot path private int _nextDupPosition; // Cached next dup position - avoids array access in hot path
@ -76,9 +77,9 @@ public static partial class AcBinaryDeserializer
_propertyNames = null; _propertyNames = null;
_stringCache = null; _stringCache = null;
// Position-based string interning fields // Position-based interning fields (shared: string + IId)
_dupData = null; _dupData = null;
_internStringCache = null; _internCache = null;
_dupCheckIndex = 0; _dupCheckIndex = 0;
_nextDupPosition = int.MaxValue; _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) if (hasInternFooter && footerPosition > 0)
{ {
ReadFooterStringIndices(footerPosition); ReadFooterIndices(footerPosition);
} }
} }
/// <summary> /// <summary>
/// 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. /// VarUInt format read into flat int[] for fast hot path access.
/// </summary> /// </summary>
private void ReadFooterStringIndices(int footerPosition) private void ReadFooterIndices(int footerPosition)
{ {
// Save current position (start of data) // Save current position (start of data)
var dataPosition = _position; var dataPosition = _position;
@ -184,7 +186,7 @@ public static partial class AcBinaryDeserializer
if (dupCount == 0) if (dupCount == 0)
{ {
_dupData = Array.Empty<int>(); _dupData = Array.Empty<int>();
_internStringCache = Array.Empty<string>(); _internCache = Array.Empty<object?>();
_nextDupPosition = int.MaxValue; _nextDupPosition = int.MaxValue;
} }
else else
@ -198,7 +200,7 @@ public static partial class AcBinaryDeserializer
_dupData[i * 2 + 1] = (int)ReadVarUInt(); // cacheIndex _dupData[i * 2 + 1] = (int)ReadVarUInt(); // cacheIndex
} }
_internStringCache = new string[dupCount]; _internCache = new object?[dupCount];
// Cache first dup position for ultra-fast hot path // Cache first dup position for ultra-fast hot path
_nextDupPosition = _dupData[0]; _nextDupPosition = _dupData[0];
} }
@ -555,14 +557,14 @@ public static partial class AcBinaryDeserializer
} }
/// <summary> /// <summary>
/// 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. /// Uses position-based check for 100% reliable cache matching.
/// Ultra-fast: single int comparison in hot path. /// Ultra-fast: single int comparison in hot path.
/// </summary> /// </summary>
/// <param name="value">The string value read from stream</param> /// <param name="value">The value read from stream (string or IId object)</param>
/// <param name="streamPosition">Stream position BEFORE reading the string (type code position)</param> /// <param name="streamPosition">Stream position BEFORE reading the value (type code position)</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public void RegisterInternedString(string value, int streamPosition) public void RegisterInternedValue(object value, int streamPosition)
{ {
// Ultra-fast hot path: single int comparison // Ultra-fast hot path: single int comparison
if (streamPosition != _nextDupPosition) if (streamPosition != _nextDupPosition)
@ -572,7 +574,7 @@ public static partial class AcBinaryDeserializer
// _dupData layout: [pos0, cacheIdx0, pos1, cacheIdx1, ...] // _dupData layout: [pos0, cacheIdx0, pos1, cacheIdx1, ...]
var data = _dupData!; var data = _dupData!;
var idx = _dupCheckIndex; 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; idx += 2;
_dupCheckIndex = idx; _dupCheckIndex = idx;
@ -587,12 +589,7 @@ public static partial class AcBinaryDeserializer
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public string GetInternedString(int cacheIndex) public string GetInternedString(int cacheIndex)
{ {
//if (_internStringCache == null || cacheIndex >= _internStringCache.Length) var result = _internCache![cacheIndex];
//{
// throw new AcBinaryDeserializationException($"Invalid interned string cache index '{cacheIndex}'.", _position);
//}
var result = _internStringCache![cacheIndex];
if (result == null) if (result == null)
{ {
throw new AcBinaryDeserializationException( throw new AcBinaryDeserializationException(
@ -600,6 +597,23 @@ public static partial class AcBinaryDeserializer
_position); _position);
} }
return (string)result;
}
/// <summary>
/// Gets an interned object by cache index (ObjectRef type code).
/// </summary>
[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; return result;
} }
@ -614,37 +628,6 @@ public static partial class AcBinaryDeserializer
return _propertyNames[index]; return _propertyNames[index];
} }
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void RegisterObject(TypeMetadataWrapper<BinaryDeserializeTypeMetadata> 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<int, object>(16);
//_objectReferences[refId] = instance;
}
//[MethodImpl(MethodImplOptions.AggressiveInlining)]
//public object? GetReferencedObject(TypeMetadataWrapper<BinaryDeserializeTypeMetadata> 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) private void EnsureAvailable(int length)
{ {
if (_position > _buffer.Length - length) if (_position > _buffer.Length - length)
@ -659,19 +642,5 @@ public static partial class AcBinaryDeserializer
return ReadStringUtf8(byteLength); return ReadStringUtf8(byteLength);
} }
#region IId Reference Cache - Delegates to ContextClass
/// <summary>
/// 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).
/// </summary>
//[MethodImpl(MethodImplOptions.AggressiveInlining)]
//public object GetOrRegisterIIdObject(object newObj, TypeMetadataWrapper<BinaryDeserializeTypeMetadata> wrapper)
//{
// return ContextClass.GetOrRegisterIIdObject(newObj, wrapper);
//}
#endregion
} }
} }

View File

@ -1,6 +1,4 @@
using System; using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
namespace AyCode.Core.Serializers.Binaries; namespace AyCode.Core.Serializers.Binaries;
@ -28,26 +26,5 @@ public static partial class AcBinaryDeserializer
Clear(); Clear();
base.Reset(options); base.Reset(options);
} }
/// <summary>
/// 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).
/// </summary>
[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
};
}
} }
} }

View File

@ -214,30 +214,21 @@ public static partial class AcBinaryDeserializer
/// </summary> /// </summary>
private static object? ReadObjectWithMapping(ref BinaryDeserializationContext context, Type destType, int[] indexMapping, int depth) 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 wrapper = context.ContextClass.GetWrapper(destType);
var metadata = wrapper.Metadata; var metadata = wrapper.Metadata;
// Handle reference ID if present // Wire format: [Object][props...] - no refId prefix in new format
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
var obj = CreateInstance(destType, metadata); var obj = CreateInstance(destType, metadata);
if (obj != null) 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); PopulateObjectWithMapping(ref context, obj, destType, indexMapping, depth);
} }
return obj; return obj;
@ -332,18 +323,16 @@ public static partial class AcBinaryDeserializer
var existingObj = propInfo.GetValue(target); var existingObj = propInfo.GetValue(target);
if (existingObj != null) if (existingObj != null)
{ {
var objStreamPos = context.Position; // position of Object type code
context.ReadByte(); // consume Object marker 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)) if (context.ContextClass.UseTypeReferenceHandling(wrapper.Metadata))
{ {
var refId = context.ReadVarInt(); context.RegisterInternedValue(existingObj, objStreamPos);
if (refId > 0)
{
context.RegisterObject(wrapper, refId, existingObj);
}
} }
// Wire format: [Object][props...] - no refId prefix in new format
PopulateObjectCore(ref context, existingObj, wrapper, nextDepth, skipDefaultWrite: false); PopulateObjectCore(ref context, existingObj, wrapper, nextDepth, skipDefaultWrite: false);
return; return;
} }

View File

@ -13,19 +13,8 @@ public static partial class AcBinaryDeserializer
{ {
#region Helper Methods #region Helper Methods
/// <summary> // ReadAndRegisterHashcodeIfNeeded removed - new format uses position-based footer for all reference tracking.
/// Reads hashcode prefix and registers object for Non-IId types with RefHandling=All. // No hashcode prefix in wire format anymore.
/// IId types have no prefix - their Id is in the properties.
/// Call this AFTER consuming the Object marker, BEFORE PopulateObjectCore.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void ReadAndRegisterHashcodeIfNeeded(ref BinaryDeserializationContext context, TypeMetadataWrapper<BinaryDeserializeTypeMetadata> wrapper, object instance)
{
if (wrapper.Metadata.IsIId || !context.ContextClass.UseTypeReferenceHandling(wrapper.Metadata)) return;
var hashcode = context.ReadVarInt();
context.ContextClass.TryGetOrStoreInt32(wrapper, instance, hashcode);
}
#endregion #endregion
@ -39,9 +28,9 @@ public static partial class AcBinaryDeserializer
/// Populate object with automatic mode detection from context. /// Populate object with automatic mode detection from context.
/// Uses IsMergeMode to determine merge behavior for IId collections. /// Uses IsMergeMode to determine merge behavior for IId collections.
/// Wire format: /// Wire format:
/// - IId types: [Object][props 0-tól...] - no refId prefix, Id is in props /// - IId types: [Object][props 0-t<EFBFBD>l...] - no refId prefix, Id is in props
/// - Non-IId + All: [Object][hashcode][props 0-tól...] - hashcode prefix /// - Non-IId + All: [Object][hashcode][props 0-t<EFBFBD>l...] - hashcode prefix
/// - Ref=Off: [Object][props 0-tól...] - no prefix /// - Ref=Off: [Object][props 0-t<EFBFBD>l...] - no prefix
/// </summary> /// </summary>
private static void PopulateObject(ref BinaryDeserializationContext context, object target, Type targetType, int depth) private static void PopulateObject(ref BinaryDeserializationContext context, object target, Type targetType, int depth)
{ {
@ -51,8 +40,8 @@ public static partial class AcBinaryDeserializer
/// <summary> /// <summary>
/// Core populate logic shared by all populate paths. /// 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). /// Wire format: All properties are written WITH type markers (including Id for IId types).
/// No hashcode prefix - position-based footer handles reference tracking.
/// </summary> /// </summary>
private static void PopulateObjectCore( private static void PopulateObjectCore(
ref BinaryDeserializationContext context, ref BinaryDeserializationContext context,
@ -61,9 +50,6 @@ public static partial class AcBinaryDeserializer
int depth, int depth,
bool skipDefaultWrite) 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); PopulateObjectProperties(ref context, target, wrapper.Metadata, depth, skipDefaultWrite);
} }

View File

@ -790,7 +790,7 @@ public static partial class AcBinaryDeserializer
var length = (int)context.ReadVarUInt(); var length = (int)context.ReadVarUInt();
if (length == 0) return string.Empty; if (length == 0) return string.Empty;
var str = context.ReadStringUtf8(length); var str = context.ReadStringUtf8(length);
context.RegisterInternedString(str, streamPosition); context.RegisterInternedValue(str, streamPosition);
return str; return str;
} }
@ -922,81 +922,14 @@ public static partial class AcBinaryDeserializer
#region Object Reading #region Object Reading
/// <summary> /// <summary>
/// Reads an ObjectRef - looks up previously registered object. /// Reads an ObjectRef - looks up previously registered object from shared intern cache.
/// Wire format: /// Wire format: [ObjectRef][VarUInt cacheIndex]
/// - IId types: [ObjectRef][Id érték] - lookup by Id
/// - Non-IId types: [ObjectRef][hashcode] - lookup by hashcode
/// </summary> /// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static object? ReadObjectRef(ref BinaryDeserializationContext context, Type targetType, int depth) private static object? ReadObjectRef(ref BinaryDeserializationContext context, Type targetType, int depth)
{ {
var wrapper = context.ContextClass.GetWrapper(targetType); var cacheIndex = (int)context.ReadVarUInt();
return ReadObjectRef(ref context, ref wrapper); return context.GetInternedObject(cacheIndex);
}
private static object? ReadObjectRef(ref BinaryDeserializationContext context, ref TypeMetadataWrapper<BinaryDeserializeTypeMetadata> 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<BinaryDeserializeTypeMetadata> 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<BinaryDeserializeTypeMetadata> 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<BinaryDeserializeTypeMetadata> 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);
} }
/// <summary> /// <summary>
@ -1008,6 +941,9 @@ public static partial class AcBinaryDeserializer
/// </summary> /// </summary>
private static object? ReadObject(ref BinaryDeserializationContext context, Type targetType, int depth) 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 // Handle dictionary types
if (IsDictionaryType(targetType, out var keyType, out var valueType)) if (IsDictionaryType(targetType, out var keyType, out var valueType))
{ {
@ -1017,51 +953,22 @@ public static partial class AcBinaryDeserializer
var wrapper = context.ContextClass.GetWrapper(targetType); var wrapper = context.ContextClass.GetWrapper(targetType);
var metadata = wrapper.Metadata; 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 (context.ContextClass.UseTypeReferenceHandling(metadata))
{ {
if (metadata.IsIId) context.RegisterInternedValue(instance, streamPosition);
{ }
// 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); 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);
}
}
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);
}
// ChainMode: Register/retrieve from chain tracker (separate from reference handling) // 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) if (context.IsChainMode && metadata.IsIId && metadata.IdType != null)
{ {
var id = GetIdBoxed(instance, metadata); var id = GetIdBoxed(instance, metadata);
@ -1424,7 +1331,7 @@ public static partial class AcBinaryDeserializer
var byteLen = (int)context.ReadVarUInt(); var byteLen = (int)context.ReadVarUInt();
if (byteLen == 0) return; if (byteLen == 0) return;
var str = context.ReadStringUtf8(byteLen); var str = context.ReadStringUtf8(byteLen);
context.RegisterInternedString(str, streamPosition); context.RegisterInternedValue(str, streamPosition);
} }
///// <summary> ///// <summary>
@ -1447,18 +1354,8 @@ public static partial class AcBinaryDeserializer
private static void SkipObject(ref BinaryDeserializationContext context, BinaryDeserializeTypeMetadata metaData) private static void SkipObject(ref BinaryDeserializationContext context, BinaryDeserializeTypeMetadata metaData)
{ {
// Wire format: // Wire format: [Object][props...] - no hashcode prefix in new 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();
}
// NEW FORMAT: Can't skip without knowing property count! // 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( throw new NotSupportedException(
"SkipObject is not supported with SKIP marker format. " + "SkipObject is not supported with SKIP marker format. " +
"Unable to determine property count without type metadata."); "Unable to determine property count without type metadata.");
@ -1487,20 +1384,6 @@ public static partial class AcBinaryDeserializer
#region IId Registration Helpers #region IId Registration Helpers
/// <summary>
/// Registers an IId instance after populate - uses pre-bound delegate, NO SWITCH!
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void RegisterIIdInstance(
BinaryDeserializationContextClass contextClass,
TypeMetadataWrapper<BinaryDeserializeTypeMetadata> wrapper,
object instance,
BinaryDeserializeTypeMetadata metadata)
{
// Pre-bound delegate - no switch needed!
wrapper.RegisterById!(instance);
}
/// <summary> /// <summary>
/// Gets Id as boxed object - only used for ChainMode (rare path). /// Gets Id as boxed object - only used for ChainMode (rare path).
/// </summary> /// </summary>

View File

@ -122,6 +122,10 @@ public static partial class AcBinarySerializer
// These properties delegate to Options for convenience // These properties delegate to Options for convenience
public bool UseStringInterning => Options.UseStringInterning != StringInterningMode.None; public bool UseStringInterning => Options.UseStringInterning != StringInterningMode.None;
/// <summary>
/// True if we need footer position in header (string interning OR reference handling).
/// </summary>
public bool HasFooter => UseStringInterning || ReferenceHandling != ReferenceHandlingMode.None;
public bool UseMetadata => Options.UseMetadata; public bool UseMetadata => Options.UseMetadata;
public byte MinStringInternLength => Options.MinStringInternLength; public byte MinStringInternLength => Options.MinStringInternLength;
public byte MaxStringInternLength => Options.MaxStringInternLength; public byte MaxStringInternLength => Options.MaxStringInternLength;
@ -263,41 +267,99 @@ public static partial class AcBinarySerializer
public int GetDupCount() => _nextCacheIndex; public int GetDupCount() => _nextCacheIndex;
/// <summary> /// <summary>
/// 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[]. /// VarUInt format for compact size, deserializer reads into flat int[].
/// </summary> /// </summary>
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) // Collect ALL entries with CacheIndex >= 0 (string + ID, all occurred more than once)
// We need to sort by StreamPosition for deserializer sequential access
Span<(int Position, int CacheIndex)> entries = _nextCacheIndex <= 64 Span<(int Position, int CacheIndex)> entries = _nextCacheIndex <= 64
? stackalloc (int, int)[_nextCacheIndex] ? stackalloc (int, int)[_nextCacheIndex]
: new (int, int)[_nextCacheIndex]; : new (int, int)[_nextCacheIndex];
var idx = 0; var idx = 0;
// 1. String intern entries
if (_stringInternMap != null)
{
var count = _stringInternMap.Count; var count = _stringInternMap.Count;
for (var i = 0; i < count; i++) for (var i = 0; i < count; i++)
{ {
ref var entry = ref _stringInternMap.GetValueRefAt(i); ref var entry = ref _stringInternMap.GetValueRefAt(i);
if (entry.CacheIndex >= 0) if (entry.CacheIndex >= 0)
{
entries[idx++] = (entry.StreamPosition, entry.CacheIndex); 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 // 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 // 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)usedEntries[i].Position);
WriteVarUInt((uint)entries[i].CacheIndex); WriteVarUInt((uint)usedEntries[i].CacheIndex);
} }
} }
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void CollectInternEntries<TKey>(IdentityMap<TKey, InternEntry>? 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)
/// <summary>
/// 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).
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool TryTrackObject(TypeMetadataWrapper<BinarySerializeTypeMetadata> wrapper, object obj, out int cacheIndex)
{
return TryTrack(wrapper, obj, _position, ref _nextCacheIndex, out cacheIndex);
}
/// <summary>
/// Tries to track an IId object (Int64 Id).
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool TryTrackObjectLong(TypeMetadataWrapper<BinarySerializeTypeMetadata> wrapper, object obj, out int cacheIndex)
{
return TryTrackLong(wrapper, obj, _position, ref _nextCacheIndex, out cacheIndex);
}
/// <summary>
/// Tries to track an IId object (Guid Id).
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool TryTrackObjectGuid(TypeMetadataWrapper<BinarySerializeTypeMetadata> wrapper, object obj, out int cacheIndex)
{
return TryTrackGuid(wrapper, obj, _position, ref _nextCacheIndex, out cacheIndex);
}
#endregion #endregion
#region Property Name Table #region Property Name Table
@ -971,10 +1033,10 @@ public static partial class AcBinarySerializer
// Header layout: // Header layout:
// [0] version (1 byte) // [0] version (1 byte)
// [1] flags (1 byte) // [1] flags (1 byte)
// [2-5] footer position (4 bytes, only if UseStringInterning) // [2-5] footer position (4 bytes, if string interning OR reference handling)
EnsureCapacity(UseStringInterning ? 6 : 2); EnsureCapacity(HasFooter ? 6 : 2);
_headerPosition = _position; _headerPosition = _position;
_position += UseStringInterning ? 6 : 2; _position += HasFooter ? 6 : 2;
} }
/// <summary> /// <summary>
@ -991,7 +1053,7 @@ public static partial class AcBinarySerializer
public void FinalizeHeaderSections() public void FinalizeHeaderSections()
{ {
var hasPropertyNames = UseMetadata && _propertyNameList is { Count: > 0 }; 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; var hasInternTable = dupCount > 0;
// Calculate property names header size (strings go to footer now) // 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 // Write property names to header if needed
var headerPayloadStart = _headerPosition + (UseStringInterning ? 6 : 2); var headerPayloadStart = _headerPosition + (HasFooter ? 6 : 2);
if (hasPropertyNames) if (hasPropertyNames)
{ {
var headerPos = headerPayloadStart; 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; var footerPosition = 0;
if (hasInternTable) if (hasInternTable)
{ {
footerPosition = _position; footerPosition = _position;
WriteFooterStringIndices(dupCount); WriteVarUInt((uint)dupCount);
WriteInternedFooter();
} }
// Write header // Write header
@ -1037,36 +1100,21 @@ public static partial class AcBinarySerializer
flags |= BinaryTypeCode.HeaderFlag_RefHandling_OnlyId; flags |= BinaryTypeCode.HeaderFlag_RefHandling_OnlyId;
else if (ReferenceHandling == ReferenceHandlingMode.All) else if (ReferenceHandling == ReferenceHandlingMode.All)
flags |= (byte)(BinaryTypeCode.HeaderFlag_RefHandling_OnlyId | BinaryTypeCode.HeaderFlag_RefHandling_All); flags |= (byte)(BinaryTypeCode.HeaderFlag_RefHandling_OnlyId | BinaryTypeCode.HeaderFlag_RefHandling_All);
// Set footer position flag if string interning is enabled // Set footer position flag if footer is needed (string interning OR ref handling)
if (UseStringInterning) if (HasFooter)
flags |= BinaryTypeCode.HeaderFlag_HasFooterPosition; flags |= BinaryTypeCode.HeaderFlag_HasFooterPosition;
_buffer[_headerPosition] = AcBinarySerializerOptions.FormatVersion; _buffer[_headerPosition] = AcBinarySerializerOptions.FormatVersion;
_buffer[_headerPosition + 1] = flags; _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) // (even if there's no actual interned data - footer position will be 0)
if (UseStringInterning) if (HasFooter)
{ {
Unsafe.WriteUnaligned(ref _buffer[_headerPosition + 2], footerPosition); Unsafe.WriteUnaligned(ref _buffer[_headerPosition + 2], footerPosition);
} }
} }
/// <summary>
/// Writes the footer with total count (for verification) + dup count + indices.
/// Footer format: [totalStringCount][dupCount][dupIndex0][dupIndex1]...
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
/// <summary>
/// Writes footer: [dupCount][(position, cacheIndex), ...]
/// Position-based format for 100% reliable deserializer matching.
/// </summary>
private void WriteFooterStringIndices(int dupCount)
{
// Dup count + (position, cacheIndex) pairs
WriteVarUInt((uint)dupCount);
WriteInternedStringFooter();
}
/// <summary> /// <summary>
/// Writes UTF8 string at specific position, optimized for ASCII strings. /// Writes UTF8 string at specific position, optimized for ASCII strings.

View File

@ -846,15 +846,15 @@ public static partial class AcBinarySerializer
{ {
if (metadata.IsIId) if (metadata.IsIId)
{ {
// IId típus: track by Id, ObjectRef writes Id // IId típus: track by Id, ObjectRef writes cacheIndex
switch (metadata.IdAccessorType) switch (metadata.IdAccessorType)
{ {
case IdAccessorType.Int32: 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.WriteByte(BinaryTypeCode.ObjectRef);
context.WriteVarInt(intId); context.WriteVarUInt((uint)cacheIndex32);
return; return;
} }
// First occurrence → Object (no extra data, Id in props) // First occurrence → Object (no extra data, Id in props)
@ -862,20 +862,20 @@ public static partial class AcBinarySerializer
break; break;
case IdAccessorType.Int64: case IdAccessorType.Int64:
if (!context.TryTrack(wrapper, value, out long longId)) if (!context.TryTrackObjectLong(wrapper, value, out int cacheIndex64))
{ {
context.WriteByte(BinaryTypeCode.ObjectRef); context.WriteByte(BinaryTypeCode.ObjectRef);
context.WriteVarLong(longId); context.WriteVarUInt((uint)cacheIndex64);
return; return;
} }
context.WriteByte(BinaryTypeCode.Object); context.WriteByte(BinaryTypeCode.Object);
break; break;
case IdAccessorType.Guid: case IdAccessorType.Guid:
if (!context.TryTrack(wrapper, value, out Guid guidId)) if (!context.TryTrackObjectGuid(wrapper, value, out int cacheIndexGuid))
{ {
context.WriteByte(BinaryTypeCode.ObjectRef); context.WriteByte(BinaryTypeCode.ObjectRef);
context.WriteGuidBits(guidId); context.WriteVarUInt((uint)cacheIndexGuid);
return; return;
} }
context.WriteByte(BinaryTypeCode.Object); context.WriteByte(BinaryTypeCode.Object);
@ -884,17 +884,16 @@ public static partial class AcBinarySerializer
} }
else else
{ {
// Non-IId + RefHandling=All: track by hashcode (IdAccessorType=Int32, RefIdGetter=GetHashCode) // Non-IId + RefHandling=All: track by hashcode
if (!context.TryTrack(wrapper, value, out int hashcode)) if (!context.TryTrackObject(wrapper, value, out int cacheIndexHash))
{ {
// Already seen → ObjectRef + hashcode // Already seen → ObjectRef + cacheIndex
context.WriteByte(BinaryTypeCode.ObjectRef); context.WriteByte(BinaryTypeCode.ObjectRef);
context.WriteVarInt(hashcode); context.WriteVarUInt((uint)cacheIndexHash);
return; return;
} }
// First occurrence → Object + hashcode + props // First occurrence → Object (no extra prefix for Non-IId in new format)
context.WriteByte(BinaryTypeCode.Object); context.WriteByte(BinaryTypeCode.Object);
context.WriteVarInt(hashcode);
} }
} }
else else

View File

@ -59,16 +59,6 @@ public interface IIdentityMap
/// <typeparam name="TValue">The value type</typeparam> /// <typeparam name="TValue">The value type</typeparam>
public sealed class IdentityMap<TKey, TValue> : IIdentityMap where TKey : notnull public sealed class IdentityMap<TKey, TValue> : 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) // Slot for hash table entries (generation needed for hash table validity)
private struct HashSlot private struct HashSlot
{ {
@ -76,10 +66,6 @@ public sealed class IdentityMap<TKey, TValue> : IIdentityMap where TKey : notnul
public int Next; // next slot index in chain (-1 = end) 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) // Hash table storage (for large ints and other types)
private int[]? _buckets; // bucket index → first entry index private int[]? _buckets; // bucket index → first entry index
private HashSlot[]? _entries; // hash table entries private HashSlot[]? _entries; // hash table entries
@ -97,32 +83,15 @@ public sealed class IdentityMap<TKey, TValue> : IIdentityMap where TKey : notnul
private static readonly bool IsString = typeof(TKey) == typeof(string); private static readonly bool IsString = typeof(TKey) == typeof(string);
private static readonly bool IsValueTypeValue = typeof(TValue).IsValueType; private static readonly bool IsValueTypeValue = typeof(TValue).IsValueType;
/// <summary>
/// Number of entries in the hash table. Use with GetValueRefAt for iteration.
/// </summary>
public int Count => _count;
public IdentityMap() public IdentityMap()
{ {
} }
/// <summary>
/// Tries to add a key to tracking (serialization).
/// Returns true if first occurrence (key was added).
/// Returns false if already seen.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool TryAddKey(TKey key)
{
// Small int fast path
if (_useSmallInt && IsInt32)
{
var intKey = Unsafe.As<TKey, int>(ref key);
if ((uint)intKey < SmallSize)
{
return TryAddSmallInt(intKey);
}
}
// Hash table path
return TryAddHash(key, out _);
}
/// <summary> /// <summary>
/// Tries to add a key and returns slot index for ref access to value. /// Tries to add a key and returns slot index for ref access to value.
/// Returns true if first occurrence (key was added). /// Returns true if first occurrence (key was added).
@ -135,32 +104,6 @@ public sealed class IdentityMap<TKey, TValue> : IIdentityMap where TKey : notnul
return TryAddHash(key, out slotIndex); return TryAddHash(key, out slotIndex);
} }
/// <summary>
/// Number of entries in the hash table. Use with GetValueRefAt for iteration.
/// </summary>
public int Count => _count;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private bool TryAddSmallInt(int key)
{
// Lazy init - ONLY bitmap for tracking
if (_smallBitmap == null)
{
_smallBitmap = ArrayPool<ulong>.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)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
private static bool KeyEquals(TKey a, TKey b) private static bool KeyEquals(TKey a, TKey b)
{ {
@ -297,96 +240,6 @@ public sealed class IdentityMap<TKey, TValue> : IIdentityMap where TKey : notnul
_keys = newKeys; _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<TKey, int>(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;
}
/// <summary>
/// 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).
/// </summary>
[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;
}
/// <summary>
/// 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.
/// </summary>
[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;
}
/// <summary>
/// Tries to get the value for a key.
/// Returns true if found, false if not.
/// </summary>
[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;
}
/// <summary> /// <summary>
/// Returns a reference to the value at the given slot index. /// Returns a reference to the value at the given slot index.
/// Use with slotIndex from TryAdd for in-place value modification. /// Use with slotIndex from TryAdd for in-place value modification.
@ -415,15 +268,8 @@ public sealed class IdentityMap<TKey, TValue> : IIdentityMap where TKey : notnul
/// <param name="preRentBuckets">If true, pre-rent arrays at next capacity (useful for async Clear to shift work from hot path)</param> /// <param name="preRentBuckets">If true, pre-rent arrays at next capacity (useful for async Clear to shift work from hot path)</param>
public void Reset(bool preRentBuckets = false) public void Reset(bool preRentBuckets = false)
{ {
if (_smallBitmap != null) if (_buckets == null) return;
{
Array.Clear(_smallBitmap, 0, SmallBitmapSize);
//ArrayPool<ulong>.Shared.Return(_smallBitmap);
//_smallBitmap = null;
}
if (_buckets != null)
{
// Small arrays: keep and clear (faster than pool round-trip) // Small arrays: keep and clear (faster than pool round-trip)
if (_bucketsLength <= InitialHashCapacity * 5) if (_bucketsLength <= InitialHashCapacity * 5)
{ {
@ -478,6 +324,5 @@ public sealed class IdentityMap<TKey, TValue> : IIdentityMap where TKey : notnul
_count = 0; _count = 0;
} }
} }
}
#endregion #endregion

View File

@ -15,94 +15,42 @@ public abstract class SerializationContextBase<TMetadata, TOptions> : AcSerializ
where TMetadata : TypeMetadataBase where TMetadata : TypeMetadataBase
where TOptions : AcSerializerOptions 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) #region Tracking - InternEntry based (serializer side)
// 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)
/// <summary> /// <summary>
/// Tries to track an object with int RefId. /// Tries to track an object with int RefId.
/// Use when wrapper.Metadata.IdAccessorType == Int32. /// Returns true if first occurrence, false if already seen (cacheIndex assigned).
/// Returns true if first occurrence. /// Uses shared nextCacheIndex counter (shared with string interning).
/// </summary> /// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool TryTrack(TypeMetadataWrapper<TMetadata> wrapper, object obj, out int refId) public bool TryTrack(TypeMetadataWrapper<TMetadata> wrapper, object obj, int streamPosition, ref int nextCacheIndex, out int cacheIndex)
{ {
Debug.Assert(wrapper.Metadata.IdAccessorType == IdAccessorType.Int32); Debug.Assert(wrapper.Metadata.IdAccessorType == IdAccessorType.Int32);
var id = wrapper.RefIdGetterInt32!(obj);
// Use pre-cast getter - no cast overhead! return wrapper.TryTrackInt32(id, streamPosition, ref nextCacheIndex, out cacheIndex);
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<TMetadata> 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<TMetadata> wrapper)
{
wrapper.SmallIdBitmap = new ulong[BitArraySize];
return wrapper.SmallIdBitmap;
} }
/// <summary> /// <summary>
/// Tries to track an object with long RefId. /// Tries to track an object with long RefId.
/// Use when wrapper.Metadata.IdAccessorType == Int64.
/// Returns true if first occurrence.
/// </summary> /// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool TryTrack(TypeMetadataWrapper<TMetadata> wrapper, object obj, out long refId) public bool TryTrackLong(TypeMetadataWrapper<TMetadata> wrapper, object obj, int streamPosition, ref int nextCacheIndex, out int cacheIndex)
{ {
Debug.Assert(wrapper.Metadata.IdAccessorType == IdAccessorType.Int64); Debug.Assert(wrapper.Metadata.IdAccessorType == IdAccessorType.Int64);
var id = wrapper.RefIdGetterInt64!(obj);
// Use pre-cast getter - no cast overhead! return wrapper.TryTrackInt64(id, streamPosition, ref nextCacheIndex, out cacheIndex);
refId = wrapper.RefIdGetterInt64!(obj);
return wrapper.TryAddKey(refId);
} }
/// <summary> /// <summary>
/// Tries to track an object with Guid RefId. /// Tries to track an object with Guid RefId.
/// Use when wrapper.Metadata.IdAccessorType == Guid.
/// Returns true if first occurrence.
/// </summary> /// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool TryTrack(TypeMetadataWrapper<TMetadata> wrapper, object obj, out Guid refId) public bool TryTrackGuid(TypeMetadataWrapper<TMetadata> wrapper, object obj, int streamPosition, ref int nextCacheIndex, out int cacheIndex)
{ {
Debug.Assert(wrapper.Metadata.IdAccessorType == IdAccessorType.Guid); Debug.Assert(wrapper.Metadata.IdAccessorType == IdAccessorType.Guid);
var id = wrapper.RefIdGetterGuid!(obj);
// Use pre-cast getter - no cast overhead! return wrapper.TryTrackGuid(id, streamPosition, ref nextCacheIndex, out cacheIndex);
refId = wrapper.RefIdGetterGuid!(obj);
return wrapper.TryAddKey(refId);
} }
#endregion #endregion

View File

@ -40,17 +40,17 @@ public sealed class TypeMetadataWrapper<TMetadata> where TMetadata : TypeMetadat
/// <summary> /// <summary>
/// Typed IdentityMap for Int32 IDs. Direct access, no type check. /// Typed IdentityMap for Int32 IDs. Direct access, no type check.
/// </summary> /// </summary>
internal IdentityMap<int, object?>? IdentityMapInt32; internal IdentityMap<int, InternEntry>? IdentityMapInt32;
/// <summary> /// <summary>
/// Typed IdentityMap for Int64 IDs. Direct access, no type check. /// Typed IdentityMap for Int64 IDs. Direct access, no type check.
/// </summary> /// </summary>
internal IdentityMap<long, object?>? IdentityMapInt64; internal IdentityMap<long, InternEntry>? IdentityMapInt64;
/// <summary> /// <summary>
/// Typed IdentityMap for Guid IDs. Direct access, no type check. /// Typed IdentityMap for Guid IDs. Direct access, no type check.
/// </summary> /// </summary>
internal IdentityMap<Guid, object?>? IdentityMapGuid; internal IdentityMap<Guid, InternEntry>? IdentityMapGuid;
#endregion #endregion
@ -68,11 +68,6 @@ public sealed class TypeMetadataWrapper<TMetadata> where TMetadata : TypeMetadat
/// </summary> /// </summary>
private static readonly Func<object, int> HashCodeGetter = RuntimeHelpers.GetHashCode; private static readonly Func<object, int> HashCodeGetter = RuntimeHelpers.GetHashCode;
/// <summary>
/// Pre-bound delegate to register instance by Id - NO SWITCH in hot path!
/// Set in constructor based on IdAccessorType. Method group conversion = no allocation.
/// </summary>
internal readonly Action<object>? RegisterById;
/// <summary> /// <summary>
/// Creates a new wrapper for the given metadata. /// Creates a new wrapper for the given metadata.
@ -86,20 +81,17 @@ public sealed class TypeMetadataWrapper<TMetadata> where TMetadata : TypeMetadat
// Use cached delegate from metadata for IId types, static fallback for non-IId // Use cached delegate from metadata for IId types, static fallback for non-IId
var refIdGetter = metadata.TypedIdGetter ?? HashCodeGetter; 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) switch (metadata.IdAccessorType)
{ {
case IdAccessorType.Int32: case IdAccessorType.Int32:
RefIdGetterInt32 = (Func<object, int>)refIdGetter; RefIdGetterInt32 = (Func<object, int>)refIdGetter;
RegisterById = RegisterByInt32Id; // Method group - no allocation!
break; break;
case IdAccessorType.Int64: case IdAccessorType.Int64:
RefIdGetterInt64 = (Func<object, long>)refIdGetter; RefIdGetterInt64 = (Func<object, long>)refIdGetter;
RegisterById = RegisterByInt64Id;
break; break;
case IdAccessorType.Guid: case IdAccessorType.Guid:
RefIdGetterGuid = (Func<object, Guid>)refIdGetter; RefIdGetterGuid = (Func<object, Guid>)refIdGetter;
RegisterById = RegisterByGuidId;
break; break;
} }
} }
@ -126,165 +118,84 @@ public sealed class TypeMetadataWrapper<TMetadata> where TMetadata : TypeMetadat
IdentityMapGuid?.Reset(preRentBuckets); IdentityMapGuid?.Reset(preRentBuckets);
} }
#region Direct Int32 Operations - No type check, no generic overhead #region Serializer: TryTrack by typed Id InternEntry with slotIndex
/// <summary> /// <summary>
/// Tries to get object by Int32 Id. Returns true if found. /// Tries to track Int32 Id. Returns true if first occurrence.
/// Direct dictionary lookup - no generic type check! /// On repeat: assigns CacheIndex from shared counter, returns cacheIndex.
/// </summary> /// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool TryGetValueInt32(int id, out object? instance) public bool TryTrackInt32(int id, int streamPosition, ref int nextCacheIndex, out int cacheIndex)
{ {
if (IdentityMapInt32 != null) if (id == 0) { cacheIndex = -1; return true; } // Default Id - no tracking
return IdentityMapInt32.TryGetValue(id, out instance);
instance = null; var map = IdentityMapInt32 ??= new IdentityMap<int, InternEntry>();
if (!map.TryAdd(id, out var slotIndex))
{
// 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; return false;
} }
/// <summary> // 1st occurrence: store stream position
/// Gets existing object or stores new one by Int32 Id. ref var newEntry = ref map.GetValueRef(slotIndex);
/// Direct dictionary access - no generic type check! newEntry.StreamPosition = streamPosition;
/// </summary> newEntry.CacheIndex = -1;
[MethodImpl(MethodImplOptions.AggressiveInlining)] cacheIndex = -1;
public object TryGetOrStoreInt32(int id, object newObj) return true;
{
if (id == 0) return newObj; // Default Id - no tracking
var map = IdentityMapInt32 ??= new IdentityMap<int, object?>();
return map.TryGetOrAddValue(id, newObj);
} }
/// <summary> /// <summary>
/// Registers IId instance by extracting its Int32 Id. /// Tries to track Int64 Id. Returns true if first occurrence.
/// Combines Id getter + store in one call.
/// </summary> /// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public void RegisterByInt32Id(object instance) public bool TryTrackInt64(long id, int streamPosition, ref int nextCacheIndex, out int cacheIndex)
{ {
var id = RefIdGetterInt32!(instance); if (id == 0) { cacheIndex = -1; return true; }
if (id == 0) return;
var map = IdentityMapInt32 ??= new IdentityMap<int, object?>(); var map = IdentityMapInt64 ??= new IdentityMap<long, InternEntry>();
map.TryGetOrAddValue(id, instance); if (!map.TryAdd(id, out var slotIndex))
}
#endregion
#region Direct Int64 Operations
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool TryGetValueInt64(long id, out object? instance)
{ {
if (IdentityMapInt64 != null) ref var entry = ref map.GetValueRef(slotIndex);
return IdentityMapInt64.TryGetValue(id, out instance); if (entry.CacheIndex < 0)
entry.CacheIndex = nextCacheIndex++;
instance = null; cacheIndex = entry.CacheIndex;
return false; return false;
} }
[MethodImpl(MethodImplOptions.AggressiveInlining)] ref var newEntry = ref map.GetValueRef(slotIndex);
public object TryGetOrStoreInt64(long id, object newObj) newEntry.StreamPosition = streamPosition;
{ newEntry.CacheIndex = -1;
if (id == 0) return newObj; cacheIndex = -1;
return true;
var map = IdentityMapInt64 ??= new IdentityMap<long, object?>();
return map.TryGetOrAddValue(id, newObj);
} }
/// <summary>
/// Tries to track Guid Id. Returns true if first occurrence.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public void RegisterByInt64Id(object instance) public bool TryTrackGuid(Guid id, int streamPosition, ref int nextCacheIndex, out int cacheIndex)
{ {
var id = RefIdGetterInt64!(instance); if (id == Guid.Empty) { cacheIndex = -1; return true; }
if (id == 0) return;
var map = IdentityMapInt64 ??= new IdentityMap<long, object?>(); var map = IdentityMapGuid ??= new IdentityMap<Guid, InternEntry>();
map.TryGetOrAddValue(id, instance); if (!map.TryAdd(id, out var slotIndex))
}
#endregion
#region Direct Guid Operations
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool TryGetValueGuid(Guid id, out object? instance)
{ {
if (IdentityMapGuid != null) ref var entry = ref map.GetValueRef(slotIndex);
return IdentityMapGuid.TryGetValue(id, out instance); if (entry.CacheIndex < 0)
entry.CacheIndex = nextCacheIndex++;
instance = null; cacheIndex = entry.CacheIndex;
return false; return false;
} }
[MethodImpl(MethodImplOptions.AggressiveInlining)] ref var newEntry = ref map.GetValueRef(slotIndex);
public object TryGetOrStoreGuid(Guid id, object newObj) newEntry.StreamPosition = streamPosition;
{ newEntry.CacheIndex = -1;
if (id == Guid.Empty) return newObj; cacheIndex = -1;
return true;
var map = IdentityMapGuid ??= new IdentityMap<Guid, object?>();
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<Guid, object?>();
map.TryGetOrAddValue(id, instance);
}
#endregion
#region Legacy Generic Methods (for backward compatibility)
/// <summary>
/// Gets or creates the typed IdentityMap for tracking.
/// SLOW PATH - use typed methods (TryGetValueInt32, etc.) instead!
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public IdentityMap<TId, object?> GetOrCreateIdentityMap<TId>() where TId : notnull
{
// Route to typed fields based on TId
if (typeof(TId) == typeof(int))
{
var map = IdentityMapInt32 ??= new IdentityMap<int, object?>();
return Unsafe.As<IdentityMap<int, object?>, IdentityMap<TId, object?>>(ref map);
}
if (typeof(TId) == typeof(long))
{
var map = IdentityMapInt64 ??= new IdentityMap<long, object?>();
return Unsafe.As<IdentityMap<long, object?>, IdentityMap<TId, object?>>(ref map);
}
if (typeof(TId) == typeof(Guid))
{
var map = IdentityMapGuid ??= new IdentityMap<Guid, object?>();
return Unsafe.As<IdentityMap<Guid, object?>, IdentityMap<TId, object?>>(ref map);
}
throw new NotSupportedException($"Id type {typeof(TId)} is not supported");
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool TryAddKey<TId>(TId key) where TId : struct
{
var map = GetOrCreateIdentityMap<TId>();
return map.TryAddKey(key);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool TryGetValue<TId>(TId refId, out object? instance) where TId : struct
{
var map = GetOrCreateIdentityMap<TId>();
return map.TryGetValue(refId, out instance);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public object TryGetOrStoreId<TId>(TId id, object newObj) where TId : struct
{
var map = GetOrCreateIdentityMap<TId>();
return map.TryGetOrAddValue(id, newObj);
} }
#endregion #endregion