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
#region Tracking API - int
[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);
}
#region Wrapper Iteration (for footer writing)
/// <summary>
/// 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.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public object TryGetOrStoreInt32(TypeMetadataWrapper<TMetadata> wrapper, object newObj, int id)
{
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);
}
public Dictionary<Type, TypeMetadataWrapper<TMetadata>>.ValueCollection GetWrappers() => _wrappers.Values;
#endregion

View File

@ -20,10 +20,11 @@ public static partial class AcBinaryDeserializer
private List<string>? _propertyNames;
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
// 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);
}
}
/// <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.
/// </summary>
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<int>();
_internStringCache = Array.Empty<string>();
_internCache = Array.Empty<object?>();
_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
}
/// <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.
/// Ultra-fast: single int comparison in hot path.
/// </summary>
/// <param name="value">The string value read from stream</param>
/// <param name="streamPosition">Stream position BEFORE reading the string (type code position)</param>
/// <param name="value">The value read from stream (string or IId object)</param>
/// <param name="streamPosition">Stream position BEFORE reading the value (type code position)</param>
[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;
}
/// <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;
}
@ -614,37 +628,6 @@ public static partial class AcBinaryDeserializer
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)
{
if (_position > _buffer.Length - length)
@ -659,19 +642,5 @@ public static partial class AcBinaryDeserializer
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.Collections.Generic;
using System.Runtime.CompilerServices;
namespace AyCode.Core.Serializers.Binaries;
@ -28,26 +26,5 @@ public static partial class AcBinaryDeserializer
Clear();
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>
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;
}

View File

@ -13,19 +13,8 @@ public static partial class AcBinaryDeserializer
{
#region Helper Methods
/// <summary>
/// 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.
/// </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);
}
// 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<EFBFBD>l...] - no refId prefix, Id is in props
/// - Non-IId + All: [Object][hashcode][props 0-t<EFBFBD>l...] - hashcode prefix
/// - Ref=Off: [Object][props 0-t<EFBFBD>l...] - no prefix
/// </summary>
private static void PopulateObject(ref BinaryDeserializationContext context, object target, Type targetType, int depth)
{
@ -51,8 +40,8 @@ public static partial class AcBinaryDeserializer
/// <summary>
/// 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.
/// </summary>
private static void PopulateObjectCore(
ref BinaryDeserializationContext context,
@ -61,9 +50,6 @@ public static partial class AcBinaryDeserializer
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);
}

View File

@ -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
/// <summary>
/// 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]
/// </summary>
[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<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);
var cacheIndex = (int)context.ReadVarUInt();
return context.GetInternedObject(cacheIndex);
}
/// <summary>
@ -1008,6 +941,9 @@ public static partial class AcBinaryDeserializer
/// </summary>
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))
{
@ -1017,51 +953,22 @@ public static partial class AcBinaryDeserializer
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;
context.RegisterInternedValue(instance, streamPosition);
}
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)
// Note: For ChainMode, we need IdGetter OR typed getters (IdAccessorType != None)
if (context.IsChainMode && metadata.IsIId && metadata.IdType != null)
{
var id = GetIdBoxed(instance, metadata);
@ -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);
}
///// <summary>
@ -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.");
@ -1487,20 +1384,6 @@ public static partial class AcBinaryDeserializer
#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>
/// Gets Id as boxed object - only used for ChainMode (rare path).
/// </summary>

View File

@ -122,6 +122,10 @@ public static partial class AcBinarySerializer
// These properties delegate to Options for convenience
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 byte MinStringInternLength => Options.MinStringInternLength;
public byte MaxStringInternLength => Options.MaxStringInternLength;
@ -263,41 +267,99 @@ public static partial class AcBinarySerializer
public int GetDupCount() => _nextCacheIndex;
/// <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[].
/// </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)
// 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;
// 1. String intern entries
if (_stringInternMap != null)
{
var count = _stringInternMap.Count;
for (var i = 0; i < count; i++)
{
ref var entry = ref _stringInternMap.GetValueRefAt(i);
if (entry.CacheIndex >= 0)
{
entries[idx++] = (entry.StreamPosition, entry.CacheIndex);
}
}
// 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<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
#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;
}
/// <summary>
@ -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);
}
}
/// <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>
/// Writes UTF8 string at specific position, optimized for ASCII strings.

View File

@ -846,15 +846,15 @@ 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)
@ -862,20 +862,20 @@ public static partial class AcBinarySerializer
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

View File

@ -59,16 +59,6 @@ public interface IIdentityMap
/// <typeparam name="TValue">The value type</typeparam>
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)
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)
}
// 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<TKey, TValue> : IIdentityMap where TKey : notnul
private static readonly bool IsString = typeof(TKey) == typeof(string);
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()
{
}
/// <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>
/// 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<TKey, TValue> : IIdentityMap where TKey : notnul
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)]
private static bool KeyEquals(TKey a, TKey b)
{
@ -297,96 +240,6 @@ public sealed class IdentityMap<TKey, TValue> : 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<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>
/// Returns a reference to the value at the given slot index.
/// 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>
public void Reset(bool preRentBuckets = false)
{
if (_smallBitmap != null)
{
Array.Clear(_smallBitmap, 0, SmallBitmapSize);
//ArrayPool<ulong>.Shared.Return(_smallBitmap);
//_smallBitmap = null;
}
if (_buckets == null) return;
if (_buckets != null)
{
// Small arrays: keep and clear (faster than pool round-trip)
if (_bucketsLength <= InitialHashCapacity * 5)
{
@ -477,7 +323,6 @@ public sealed class IdentityMap<TKey, TValue> : IIdentityMap where TKey : notnul
}
_count = 0;
}
}
}
#endregion

View File

@ -15,94 +15,42 @@ public abstract class SerializationContextBase<TMetadata, TOptions> : 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)
/// <summary>
/// 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).
/// </summary>
[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);
// 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<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;
var id = wrapper.RefIdGetterInt32!(obj);
return wrapper.TryTrackInt32(id, streamPosition, ref nextCacheIndex, out cacheIndex);
}
/// <summary>
/// Tries to track an object with long RefId.
/// Use when wrapper.Metadata.IdAccessorType == Int64.
/// Returns true if first occurrence.
/// </summary>
[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);
// 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);
}
/// <summary>
/// Tries to track an object with Guid RefId.
/// Use when wrapper.Metadata.IdAccessorType == Guid.
/// Returns true if first occurrence.
/// </summary>
[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);
// 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

View File

@ -40,17 +40,17 @@ public sealed class TypeMetadataWrapper<TMetadata> where TMetadata : TypeMetadat
/// <summary>
/// Typed IdentityMap for Int32 IDs. Direct access, no type check.
/// </summary>
internal IdentityMap<int, object?>? IdentityMapInt32;
internal IdentityMap<int, InternEntry>? IdentityMapInt32;
/// <summary>
/// Typed IdentityMap for Int64 IDs. Direct access, no type check.
/// </summary>
internal IdentityMap<long, object?>? IdentityMapInt64;
internal IdentityMap<long, InternEntry>? IdentityMapInt64;
/// <summary>
/// Typed IdentityMap for Guid IDs. Direct access, no type check.
/// </summary>
internal IdentityMap<Guid, object?>? IdentityMapGuid;
internal IdentityMap<Guid, InternEntry>? IdentityMapGuid;
#endregion
@ -68,11 +68,6 @@ public sealed class TypeMetadataWrapper<TMetadata> where TMetadata : TypeMetadat
/// </summary>
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>
/// 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
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<object, int>)refIdGetter;
RegisterById = RegisterByInt32Id; // Method group - no allocation!
break;
case IdAccessorType.Int64:
RefIdGetterInt64 = (Func<object, long>)refIdGetter;
RegisterById = RegisterByInt64Id;
break;
case IdAccessorType.Guid:
RefIdGetterGuid = (Func<object, Guid>)refIdGetter;
RegisterById = RegisterByGuidId;
break;
}
}
@ -126,165 +118,84 @@ public sealed class TypeMetadataWrapper<TMetadata> where TMetadata : TypeMetadat
IdentityMapGuid?.Reset(preRentBuckets);
}
#region Direct Int32 Operations - No type check, no generic overhead
#region Serializer: TryTrack by typed Id InternEntry with slotIndex
/// <summary>
/// Tries to get object by Int32 Id. Returns true if found.
/// Direct dictionary lookup - no generic type check!
/// Tries to track Int32 Id. Returns true if first occurrence.
/// On repeat: assigns CacheIndex from shared counter, returns cacheIndex.
/// </summary>
[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)
return IdentityMapInt32.TryGetValue(id, out instance);
if (id == 0) { cacheIndex = -1; return true; } // Default Id - no tracking
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;
}
/// <summary>
/// Gets existing object or stores new one by Int32 Id.
/// Direct dictionary access - no generic type check!
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public object TryGetOrStoreInt32(int id, object newObj)
{
if (id == 0) return newObj; // Default Id - no tracking
var map = IdentityMapInt32 ??= new IdentityMap<int, object?>();
return map.TryGetOrAddValue(id, newObj);
// 1st occurrence: store stream position
ref var newEntry = ref map.GetValueRef(slotIndex);
newEntry.StreamPosition = streamPosition;
newEntry.CacheIndex = -1;
cacheIndex = -1;
return true;
}
/// <summary>
/// Registers IId instance by extracting its Int32 Id.
/// Combines Id getter + store in one call.
/// Tries to track Int64 Id. Returns true if first occurrence.
/// </summary>
[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) return;
if (id == 0) { cacheIndex = -1; return true; }
var map = IdentityMapInt32 ??= new IdentityMap<int, object?>();
map.TryGetOrAddValue(id, instance);
}
#endregion
#region Direct Int64 Operations
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool TryGetValueInt64(long id, out object? instance)
var map = IdentityMapInt64 ??= new IdentityMap<long, InternEntry>();
if (!map.TryAdd(id, out var slotIndex))
{
if (IdentityMapInt64 != null)
return IdentityMapInt64.TryGetValue(id, out instance);
instance = null;
ref var entry = ref map.GetValueRef(slotIndex);
if (entry.CacheIndex < 0)
entry.CacheIndex = nextCacheIndex++;
cacheIndex = entry.CacheIndex;
return false;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public object TryGetOrStoreInt64(long id, object newObj)
{
if (id == 0) return newObj;
var map = IdentityMapInt64 ??= new IdentityMap<long, object?>();
return map.TryGetOrAddValue(id, newObj);
ref var newEntry = ref map.GetValueRef(slotIndex);
newEntry.StreamPosition = streamPosition;
newEntry.CacheIndex = -1;
cacheIndex = -1;
return true;
}
/// <summary>
/// Tries to track Guid Id. Returns true if first occurrence.
/// </summary>
[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 == 0) return;
if (id == Guid.Empty) { cacheIndex = -1; return true; }
var map = IdentityMapInt64 ??= new IdentityMap<long, object?>();
map.TryGetOrAddValue(id, instance);
}
#endregion
#region Direct Guid Operations
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool TryGetValueGuid(Guid id, out object? instance)
var map = IdentityMapGuid ??= new IdentityMap<Guid, InternEntry>();
if (!map.TryAdd(id, out var slotIndex))
{
if (IdentityMapGuid != null)
return IdentityMapGuid.TryGetValue(id, out instance);
instance = null;
ref var entry = ref map.GetValueRef(slotIndex);
if (entry.CacheIndex < 0)
entry.CacheIndex = nextCacheIndex++;
cacheIndex = entry.CacheIndex;
return false;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public object TryGetOrStoreGuid(Guid id, object newObj)
{
if (id == Guid.Empty) return newObj;
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);
ref var newEntry = ref map.GetValueRef(slotIndex);
newEntry.StreamPosition = streamPosition;
newEntry.CacheIndex = -1;
cacheIndex = -1;
return true;
}
#endregion