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:
parent
23af1fc98b
commit
9b151fd6cf
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,19 +40,16 @@ 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,
|
||||
object target,
|
||||
TypeMetadataWrapper<BinaryDeserializeTypeMetadata> wrapper,
|
||||
int depth,
|
||||
ref BinaryDeserializationContext context,
|
||||
object target,
|
||||
TypeMetadataWrapper<BinaryDeserializeTypeMetadata> wrapper,
|
||||
int depth,
|
||||
bool skipDefaultWrite)
|
||||
{
|
||||
// Handle hashcode for Non-IId types - ONE place for this logic!
|
||||
ReadAndRegisterHashcodeIfNeeded(ref context, wrapper, target);
|
||||
|
||||
PopulateObjectProperties(ref context, target, wrapper.Metadata, depth, skipDefaultWrite);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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,60 +941,34 @@ 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))
|
||||
{
|
||||
return ReadDictionaryAsObject(ref context, keyType!, valueType!, depth);
|
||||
}
|
||||
|
||||
|
||||
var wrapper = context.ContextClass.GetWrapper(targetType);
|
||||
var metadata = wrapper.Metadata;
|
||||
|
||||
object? instance;
|
||||
// Wire format: [Object][props...] - no hashcode prefix, no Id prefix
|
||||
// Position-based footer handles all reference tracking
|
||||
var instance = CreateInstance(targetType, metadata);
|
||||
if (instance == null) return null;
|
||||
|
||||
// Register in shared intern cache BEFORE populate (position-based sequential check)
|
||||
// Instance is populated in-place, so registering early is safe.
|
||||
// Must happen before reading inner content (strings/objects) that may also need registration.
|
||||
if (context.ContextClass.UseTypeReferenceHandling(metadata))
|
||||
{
|
||||
if (metadata.IsIId)
|
||||
{
|
||||
// IId: [Object][props 0-tól...]
|
||||
// Create → Populate (Id beolvasódik) → Register by Id
|
||||
instance = CreateInstance(targetType, metadata);
|
||||
if (instance == null) return null;
|
||||
|
||||
PopulateObject(ref context, instance, metadata, depth, skipDefaultWrite: true);
|
||||
|
||||
// Register by Id after populate - uses typed methods on wrapper
|
||||
RegisterIIdInstance(context.ContextClass, wrapper, instance, metadata);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Non-IId + All: [Object][hashcode][props 0-tól...]
|
||||
var hashcode = context.ReadVarInt();
|
||||
|
||||
// TryGetValue - if already exists, return (shouldn't happen for Object, only ObjectRef)
|
||||
if (wrapper.TryGetValueInt32(hashcode, out instance))
|
||||
return instance;
|
||||
|
||||
// Create + Register by hashcode (always Int32 for Non-IId)
|
||||
instance = CreateInstance(targetType, metadata);
|
||||
if (instance == null) return null;
|
||||
wrapper.TryGetOrStoreInt32(hashcode, instance);
|
||||
|
||||
// Populate
|
||||
PopulateObject(ref context, instance, metadata, depth, skipDefaultWrite: true);
|
||||
}
|
||||
context.RegisterInternedValue(instance, streamPosition);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Ref=Off: [Object][props 0-tól...]
|
||||
instance = CreateInstance(targetType, metadata);
|
||||
if (instance == null) return null;
|
||||
|
||||
PopulateObject(ref context, instance, metadata, depth, skipDefaultWrite: true);
|
||||
}
|
||||
|
||||
|
||||
PopulateObject(ref context, instance, metadata, depth, skipDefaultWrite: true);
|
||||
|
||||
// ChainMode: Register/retrieve from chain tracker (separate from reference handling)
|
||||
// Note: For ChainMode, we need IdGetter OR typed getters (IdAccessorType != None)
|
||||
if (context.IsChainMode && metadata.IsIId && metadata.IdType != null)
|
||||
{
|
||||
var id = GetIdBoxed(instance, metadata);
|
||||
|
|
@ -1074,12 +981,12 @@ public static partial class AcBinaryDeserializer
|
|||
CopyProperties(instance, existingObj!, metadata);
|
||||
return existingObj;
|
||||
}
|
||||
|
||||
|
||||
// Register this new object
|
||||
context.ChainTracker.TryRegisterIIdObject(instance);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return instance;
|
||||
}
|
||||
|
||||
|
|
@ -1424,7 +1331,7 @@ public static partial class AcBinaryDeserializer
|
|||
var byteLen = (int)context.ReadVarUInt();
|
||||
if (byteLen == 0) return;
|
||||
var str = context.ReadStringUtf8(byteLen);
|
||||
context.RegisterInternedString(str, streamPosition);
|
||||
context.RegisterInternedValue(str, streamPosition);
|
||||
}
|
||||
|
||||
///// <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.");
|
||||
|
|
@ -1486,21 +1383,7 @@ public static partial class AcBinaryDeserializer
|
|||
#endregion
|
||||
|
||||
#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>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
var count = _stringInternMap.Count;
|
||||
for (var i = 0; i < count; i++)
|
||||
|
||||
// 1. String intern entries
|
||||
if (_stringInternMap != null)
|
||||
{
|
||||
ref var entry = ref _stringInternMap.GetValueRefAt(i);
|
||||
if (entry.CacheIndex >= 0)
|
||||
var count = _stringInternMap.Count;
|
||||
for (var i = 0; i < count; i++)
|
||||
{
|
||||
entries[idx++] = (entry.StreamPosition, entry.CacheIndex);
|
||||
ref var entry = ref _stringInternMap.GetValueRefAt(i);
|
||||
if (entry.CacheIndex >= 0)
|
||||
entries[idx++] = (entry.StreamPosition, entry.CacheIndex);
|
||||
}
|
||||
}
|
||||
|
||||
// 2. ID tracking entries from all wrappers
|
||||
foreach (var wrapper in GetWrappers())
|
||||
{
|
||||
CollectInternEntries(wrapper.IdentityMapInt32, ref entries, ref idx);
|
||||
CollectInternEntries(wrapper.IdentityMapInt64, ref entries, ref idx);
|
||||
CollectInternEntries(wrapper.IdentityMapGuid, ref entries, ref idx);
|
||||
}
|
||||
|
||||
// Sort by StreamPosition (ascending) for deserializer sequential check
|
||||
entries.Sort((a, b) => a.Position.CompareTo(b.Position));
|
||||
var usedEntries = entries.Slice(0, idx);
|
||||
usedEntries.Sort((a, b) => a.Position.CompareTo(b.Position));
|
||||
|
||||
// Write pairs as VarUInt for compact size
|
||||
for (var i = 0; i < _nextCacheIndex; i++)
|
||||
for (var i = 0; i < idx; i++)
|
||||
{
|
||||
WriteVarUInt((uint)entries[i].Position);
|
||||
WriteVarUInt((uint)entries[i].CacheIndex);
|
||||
WriteVarUInt((uint)usedEntries[i].Position);
|
||||
WriteVarUInt((uint)usedEntries[i].CacheIndex);
|
||||
}
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static void CollectInternEntries<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.
|
||||
|
|
|
|||
|
|
@ -846,36 +846,36 @@ public static partial class AcBinarySerializer
|
|||
{
|
||||
if (metadata.IsIId)
|
||||
{
|
||||
// IId típus: track by Id, ObjectRef writes Id
|
||||
// IId típus: track by Id, ObjectRef writes cacheIndex
|
||||
switch (metadata.IdAccessorType)
|
||||
{
|
||||
case IdAccessorType.Int32:
|
||||
if (!context.TryTrack(wrapper, value, out int intId))
|
||||
if (!context.TryTrackObject(wrapper, value, out int cacheIndex32))
|
||||
{
|
||||
// Already seen → ObjectRef + Id
|
||||
// Already seen → ObjectRef + cacheIndex
|
||||
context.WriteByte(BinaryTypeCode.ObjectRef);
|
||||
context.WriteVarInt(intId);
|
||||
context.WriteVarUInt((uint)cacheIndex32);
|
||||
return;
|
||||
}
|
||||
// First occurrence → Object (no extra data, Id in props)
|
||||
context.WriteByte(BinaryTypeCode.Object);
|
||||
break;
|
||||
|
||||
|
||||
case IdAccessorType.Int64:
|
||||
if (!context.TryTrack(wrapper, value, out long longId))
|
||||
if (!context.TryTrackObjectLong(wrapper, value, out int cacheIndex64))
|
||||
{
|
||||
context.WriteByte(BinaryTypeCode.ObjectRef);
|
||||
context.WriteVarLong(longId);
|
||||
context.WriteVarUInt((uint)cacheIndex64);
|
||||
return;
|
||||
}
|
||||
context.WriteByte(BinaryTypeCode.Object);
|
||||
break;
|
||||
|
||||
|
||||
case IdAccessorType.Guid:
|
||||
if (!context.TryTrack(wrapper, value, out Guid guidId))
|
||||
if (!context.TryTrackObjectGuid(wrapper, value, out int cacheIndexGuid))
|
||||
{
|
||||
context.WriteByte(BinaryTypeCode.ObjectRef);
|
||||
context.WriteGuidBits(guidId);
|
||||
context.WriteVarUInt((uint)cacheIndexGuid);
|
||||
return;
|
||||
}
|
||||
context.WriteByte(BinaryTypeCode.Object);
|
||||
|
|
@ -884,17 +884,16 @@ public static partial class AcBinarySerializer
|
|||
}
|
||||
else
|
||||
{
|
||||
// Non-IId + RefHandling=All: track by hashcode (IdAccessorType=Int32, RefIdGetter=GetHashCode)
|
||||
if (!context.TryTrack(wrapper, value, out int hashcode))
|
||||
// Non-IId + RefHandling=All: track by hashcode
|
||||
if (!context.TryTrackObject(wrapper, value, out int cacheIndexHash))
|
||||
{
|
||||
// Already seen → ObjectRef + hashcode
|
||||
// Already seen → ObjectRef + cacheIndex
|
||||
context.WriteByte(BinaryTypeCode.ObjectRef);
|
||||
context.WriteVarInt(hashcode);
|
||||
context.WriteVarUInt((uint)cacheIndexHash);
|
||||
return;
|
||||
}
|
||||
// First occurrence → Object + hashcode + props
|
||||
// First occurrence → Object (no extra prefix for Non-IId in new format)
|
||||
context.WriteByte(BinaryTypeCode.Object);
|
||||
context.WriteVarInt(hashcode);
|
||||
}
|
||||
}
|
||||
else
|
||||
|
|
|
|||
|
|
@ -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,68 +268,60 @@ 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)
|
||||
if (_buckets == null) return;
|
||||
|
||||
// Small arrays: keep and clear (faster than pool round-trip)
|
||||
if (_bucketsLength <= InitialHashCapacity * 5)
|
||||
{
|
||||
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)
|
||||
if (_bucketsLength <= InitialHashCapacity * 5)
|
||||
{
|
||||
Array.Fill(_buckets, -1, 0, _bucketsLength);
|
||||
// Clear to release object references (GC can collect)
|
||||
if (_count > 0)
|
||||
{
|
||||
Array.Clear(_entries!, 0, _count);
|
||||
Array.Clear(_keys!, 0, _count);
|
||||
}
|
||||
_count = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
// Large arrays: return to pool, remember half capacity
|
||||
var nextCapacity = Math.Max(_bucketsLength / 2, InitialHashCapacity * 5);
|
||||
|
||||
// Clear entries/keys to release object references before returning to pool
|
||||
// Otherwise pool holds refs → GC can't collect!
|
||||
Array.Fill(_buckets, -1, 0, _bucketsLength);
|
||||
// Clear to release object references (GC can collect)
|
||||
if (_count > 0)
|
||||
{
|
||||
Array.Clear(_entries!, 0, _count);
|
||||
Array.Clear(_keys!, 0, _count);
|
||||
}
|
||||
|
||||
ArrayPool<int>.Shared.Return(_buckets);
|
||||
ArrayPool<HashSlot>.Shared.Return(_entries!,false);
|
||||
ArrayPool<TKey>.Shared.Return(_keys!, false);
|
||||
|
||||
if (preRentBuckets)
|
||||
{
|
||||
// Pre-rent arrays now (async background) so Pool.Get() is faster
|
||||
_buckets = ArrayPool<int>.Shared.Rent(nextCapacity);
|
||||
_bucketsLength = _buckets.Length;
|
||||
Array.Fill(_buckets, -1, 0, _bucketsLength);
|
||||
|
||||
_entries = ArrayPool<HashSlot>.Shared.Rent(nextCapacity);
|
||||
_entriesLength = _entries.Length;
|
||||
//Array.Clear(_entries, 0, _entriesLength);
|
||||
|
||||
_keys = ArrayPool<TKey>.Shared.Rent(nextCapacity);
|
||||
//Array.Clear(_keys, 0, nextCapacity);
|
||||
}
|
||||
else
|
||||
{
|
||||
_buckets = null;
|
||||
_entries = null;
|
||||
_keys = null;
|
||||
_bucketsLength = nextCapacity; // Remember for next InitHashTable
|
||||
_entriesLength = 0;
|
||||
}
|
||||
_count = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
// Large arrays: return to pool, remember half capacity
|
||||
var nextCapacity = Math.Max(_bucketsLength / 2, InitialHashCapacity * 5);
|
||||
|
||||
// Clear entries/keys to release object references before returning to pool
|
||||
// Otherwise pool holds refs → GC can't collect!
|
||||
if (_count > 0)
|
||||
{
|
||||
Array.Clear(_entries!, 0, _count);
|
||||
Array.Clear(_keys!, 0, _count);
|
||||
}
|
||||
|
||||
ArrayPool<int>.Shared.Return(_buckets);
|
||||
ArrayPool<HashSlot>.Shared.Return(_entries!,false);
|
||||
ArrayPool<TKey>.Shared.Return(_keys!, false);
|
||||
|
||||
if (preRentBuckets)
|
||||
{
|
||||
// Pre-rent arrays now (async background) so Pool.Get() is faster
|
||||
_buckets = ArrayPool<int>.Shared.Rent(nextCapacity);
|
||||
_bucketsLength = _buckets.Length;
|
||||
Array.Fill(_buckets, -1, 0, _bucketsLength);
|
||||
|
||||
_entries = ArrayPool<HashSlot>.Shared.Rent(nextCapacity);
|
||||
_entriesLength = _entries.Length;
|
||||
//Array.Clear(_entries, 0, _entriesLength);
|
||||
|
||||
_keys = ArrayPool<TKey>.Shared.Rent(nextCapacity);
|
||||
//Array.Clear(_keys, 0, nextCapacity);
|
||||
}
|
||||
else
|
||||
{
|
||||
_buckets = null;
|
||||
_entries = null;
|
||||
_keys = null;
|
||||
_bucketsLength = nextCapacity; // Remember for next InitHashTable
|
||||
_entriesLength = 0;
|
||||
}
|
||||
_count = 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -36,21 +36,21 @@ public sealed class TypeMetadataWrapper<TMetadata> where TMetadata : TypeMetadat
|
|||
internal readonly Func<object, Guid>? RefIdGetterGuid;
|
||||
|
||||
#region Typed IdentityMaps - No generic type checks in hot path!
|
||||
|
||||
|
||||
/// <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.
|
||||
|
|
@ -85,21 +80,18 @@ 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,166 +118,85 @@ public sealed class TypeMetadataWrapper<TMetadata> where TMetadata : TypeMetadat
|
|||
IdentityMapGuid?.Reset(preRentBuckets);
|
||||
}
|
||||
|
||||
#region Direct Int32 Operations - No type check, no generic overhead
|
||||
|
||||
/// <summary>
|
||||
/// Tries to get object by Int32 Id. Returns true if found.
|
||||
/// Direct dictionary lookup - no generic type check!
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public bool TryGetValueInt32(int id, out object? instance)
|
||||
{
|
||||
if (IdentityMapInt32 != null)
|
||||
return IdentityMapInt32.TryGetValue(id, out instance);
|
||||
|
||||
instance = null;
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Registers IId instance by extracting its Int32 Id.
|
||||
/// Combines Id getter + store in one call.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void RegisterByInt32Id(object instance)
|
||||
{
|
||||
var id = RefIdGetterInt32!(instance);
|
||||
if (id == 0) return;
|
||||
|
||||
var map = IdentityMapInt32 ??= new IdentityMap<int, object?>();
|
||||
map.TryGetOrAddValue(id, instance);
|
||||
}
|
||||
|
||||
#endregion
|
||||
#region Serializer: TryTrack by typed Id → InternEntry with slotIndex
|
||||
|
||||
#region Direct Int64 Operations
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public bool TryGetValueInt64(long id, out object? instance)
|
||||
{
|
||||
if (IdentityMapInt64 != null)
|
||||
return IdentityMapInt64.TryGetValue(id, out instance);
|
||||
|
||||
instance = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public object TryGetOrStoreInt64(long id, object newObj)
|
||||
{
|
||||
if (id == 0) return newObj;
|
||||
|
||||
var map = IdentityMapInt64 ??= new IdentityMap<long, object?>();
|
||||
return map.TryGetOrAddValue(id, newObj);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void RegisterByInt64Id(object instance)
|
||||
{
|
||||
var id = RefIdGetterInt64!(instance);
|
||||
if (id == 0) return;
|
||||
|
||||
var map = IdentityMapInt64 ??= new IdentityMap<long, object?>();
|
||||
map.TryGetOrAddValue(id, instance);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Direct Guid Operations
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public bool TryGetValueGuid(Guid id, out object? instance)
|
||||
{
|
||||
if (IdentityMapGuid != null)
|
||||
return IdentityMapGuid.TryGetValue(id, out instance);
|
||||
|
||||
instance = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public object TryGetOrStoreGuid(Guid id, object newObj)
|
||||
{
|
||||
if (id == Guid.Empty) return newObj;
|
||||
|
||||
var map = IdentityMapGuid ??= new IdentityMap<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!
|
||||
/// Tries to track Int32 Id. Returns true if first occurrence.
|
||||
/// On repeat: assigns CacheIndex from shared counter, returns cacheIndex.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public IdentityMap<TId, object?> GetOrCreateIdentityMap<TId>() where TId : notnull
|
||||
public bool TryTrackInt32(int id, int streamPosition, ref int nextCacheIndex, out int cacheIndex)
|
||||
{
|
||||
// Route to typed fields based on TId
|
||||
if (typeof(TId) == typeof(int))
|
||||
if (id == 0) { cacheIndex = -1; return true; } // Default Id - no tracking
|
||||
|
||||
var map = IdentityMapInt32 ??= new IdentityMap<int, InternEntry>();
|
||||
if (!map.TryAdd(id, out var slotIndex))
|
||||
{
|
||||
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);
|
||||
// 2+ occurrence: assign CacheIndex if first repeat
|
||||
ref var entry = ref map.GetValueRef(slotIndex);
|
||||
if (entry.CacheIndex < 0)
|
||||
entry.CacheIndex = nextCacheIndex++;
|
||||
cacheIndex = entry.CacheIndex;
|
||||
return false;
|
||||
}
|
||||
|
||||
throw new NotSupportedException($"Id type {typeof(TId)} is not supported");
|
||||
// 1st occurrence: store stream position
|
||||
ref var newEntry = ref map.GetValueRef(slotIndex);
|
||||
newEntry.StreamPosition = streamPosition;
|
||||
newEntry.CacheIndex = -1;
|
||||
cacheIndex = -1;
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to track Int64 Id. Returns true if first occurrence.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public bool TryAddKey<TId>(TId key) where TId : struct
|
||||
public bool TryTrackInt64(long id, int streamPosition, ref int nextCacheIndex, out int cacheIndex)
|
||||
{
|
||||
var map = GetOrCreateIdentityMap<TId>();
|
||||
return map.TryAddKey(key);
|
||||
if (id == 0) { cacheIndex = -1; return true; }
|
||||
|
||||
var map = IdentityMapInt64 ??= new IdentityMap<long, InternEntry>();
|
||||
if (!map.TryAdd(id, out var slotIndex))
|
||||
{
|
||||
ref var entry = ref map.GetValueRef(slotIndex);
|
||||
if (entry.CacheIndex < 0)
|
||||
entry.CacheIndex = nextCacheIndex++;
|
||||
cacheIndex = entry.CacheIndex;
|
||||
return false;
|
||||
}
|
||||
|
||||
ref var newEntry = ref map.GetValueRef(slotIndex);
|
||||
newEntry.StreamPosition = streamPosition;
|
||||
newEntry.CacheIndex = -1;
|
||||
cacheIndex = -1;
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to track Guid Id. Returns true if first occurrence.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public bool TryGetValue<TId>(TId refId, out object? instance) where TId : struct
|
||||
public bool TryTrackGuid(Guid id, int streamPosition, ref int nextCacheIndex, out int cacheIndex)
|
||||
{
|
||||
var map = GetOrCreateIdentityMap<TId>();
|
||||
return map.TryGetValue(refId, out instance);
|
||||
if (id == Guid.Empty) { cacheIndex = -1; return true; }
|
||||
|
||||
var map = IdentityMapGuid ??= new IdentityMap<Guid, InternEntry>();
|
||||
if (!map.TryAdd(id, out var slotIndex))
|
||||
{
|
||||
ref var entry = ref map.GetValueRef(slotIndex);
|
||||
if (entry.CacheIndex < 0)
|
||||
entry.CacheIndex = nextCacheIndex++;
|
||||
cacheIndex = entry.CacheIndex;
|
||||
return false;
|
||||
}
|
||||
|
||||
ref var newEntry = ref map.GetValueRef(slotIndex);
|
||||
newEntry.StreamPosition = streamPosition;
|
||||
newEntry.CacheIndex = -1;
|
||||
cacheIndex = -1;
|
||||
return true;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public object TryGetOrStoreId<TId>(TId id, object newObj) where TId : struct
|
||||
{
|
||||
var map = GetOrCreateIdentityMap<TId>();
|
||||
return map.TryGetOrAddValue(id, newObj);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue