Optimize string interning hot path in deserializer

Refactored AcBinaryDeserializer to use a cached _nextDupPosition
for ultra-fast string interning (single int comparison). Updated
initialization/reset logic and streamlined RegisterInternedString
to avoid unnecessary array access and branching. Commented out
MinStringInternLength threshold checks to always register interned
strings. Simplified GetInternedString and made a minor update to
the serializer's analysis report wording. These changes improve
performance and reliability of string interning during
deserialization.
This commit is contained in:
Loretta 2026-01-27 17:30:37 +01:00
parent 466782007d
commit f313d5d9ea
3 changed files with 56 additions and 48 deletions

View File

@ -33,6 +33,7 @@ public static partial class AcBinaryDeserializer
private DupEntry[]? _dupEntries; // Footer: (position, cacheIndex) pairs sorted by position private DupEntry[]? _dupEntries; // Footer: (position, cacheIndex) pairs sorted by position
private string[]? _internStringCache; // Cache for duplicated strings only private string[]? _internStringCache; // Cache for duplicated strings only
private int _dupCheckIndex; // Current position in _dupEntries private int _dupCheckIndex; // Current position in _dupEntries
private int _nextDupPosition; // Cached next dup position - avoids array access in hot path
/// <summary> /// <summary>
/// Heap-allocated context class for IId-based reference tracking. /// Heap-allocated context class for IId-based reference tracking.
@ -87,6 +88,7 @@ public static partial class AcBinaryDeserializer
_dupEntries = null; _dupEntries = null;
_internStringCache = null; _internStringCache = null;
_dupCheckIndex = 0; _dupCheckIndex = 0;
_nextDupPosition = int.MaxValue;
HasMetadata = false; HasMetadata = false;
IsMergeMode = false; IsMergeMode = false;
@ -191,6 +193,7 @@ public static partial class AcBinaryDeserializer
{ {
_dupEntries = Array.Empty<DupEntry>(); _dupEntries = Array.Empty<DupEntry>();
_internStringCache = Array.Empty<string>(); _internStringCache = Array.Empty<string>();
_nextDupPosition = int.MaxValue;
} }
else else
{ {
@ -204,6 +207,8 @@ public static partial class AcBinaryDeserializer
// Cache size: dupCount (cacheIndex is always 0, 1, 2, ..., dupCount-1) // Cache size: dupCount (cacheIndex is always 0, 1, 2, ..., dupCount-1)
_internStringCache = new string[dupCount]; _internStringCache = new string[dupCount];
// Cache first dup position for ultra-fast hot path
_nextDupPosition = _dupEntries[0].Position;
} }
// Seek back to data position // Seek back to data position
@ -560,24 +565,27 @@ public static partial class AcBinaryDeserializer
/// <summary> /// <summary>
/// Registers an interned string during body read (StringInternNew). /// Registers an interned string during body read (StringInternNew).
/// Uses position-based check for 100% reliable cache matching. /// Uses position-based check for 100% reliable cache matching.
/// Ultra-fast: single int comparison in hot path.
/// </summary> /// </summary>
/// <param name="value">The string value read from stream</param> /// <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="streamPosition">Stream position BEFORE reading the string (type code position)</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public void RegisterInternedString(string value, int streamPosition) public void RegisterInternedString(string value, int streamPosition)
{ {
// Fast path: no duplicates or already processed all // Ultra-fast hot path: single int comparison
var entries = _dupEntries; if (streamPosition != _nextDupPosition)
if (entries == null || (uint)_dupCheckIndex >= (uint)entries.Length)
return; return;
// Check if this position matches the next expected duplicate // Match! Store in cache and advance to next dup position
ref var entry = ref entries[_dupCheckIndex]; var entries = _dupEntries!;
if (entry.Position == streamPosition) var idx = _dupCheckIndex;
{ _internStringCache![entries[idx].CacheIndex] = value;
_internStringCache![entry.CacheIndex] = value;
_dupCheckIndex++; idx++;
} _dupCheckIndex = idx;
_nextDupPosition = idx < entries.Length
? entries[idx].Position
: int.MaxValue;
} }
/// <summary> /// <summary>
@ -586,12 +594,12 @@ public static partial class AcBinaryDeserializer
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public string GetInternedString(int cacheIndex) public string GetInternedString(int cacheIndex)
{ {
if (_internStringCache == null || (uint)cacheIndex >= (uint)_internStringCache.Length) //if (_internStringCache == null || cacheIndex >= _internStringCache.Length)
{ //{
throw new AcBinaryDeserializationException($"Invalid interned string cache index '{cacheIndex}'.", _position); // throw new AcBinaryDeserializationException($"Invalid interned string cache index '{cacheIndex}'.", _position);
} //}
var result = _internStringCache[cacheIndex]; var result = _internStringCache![cacheIndex];
if (result == null) if (result == null)
{ {
throw new AcBinaryDeserializationException( throw new AcBinaryDeserializationException(

View File

@ -794,23 +794,23 @@ public static partial class AcBinaryDeserializer
return str; return str;
} }
/// <summary> ///// <summary>
/// Read a string and register it in the intern table for future references. ///// Read a string and register it in the intern table for future references.
/// </summary> ///// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)] //[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static string ReadAndInternString(ref BinaryDeserializationContext context, int streamPosition) //private static string ReadAndInternString(ref BinaryDeserializationContext context, int streamPosition)
{ //{
var length = (int)context.ReadVarUInt(); // var length = (int)context.ReadVarUInt();
if (length == 0) return string.Empty; // if (length == 0) return string.Empty;
var str = context.ReadStringUtf8(length); // var str = context.ReadStringUtf8(length);
// Always register strings that meet the minimum intern length threshold // // Always register strings that meet the minimum intern length threshold
if (str.Length >= context.MinStringInternLength) // if (str.Length >= context.MinStringInternLength)
{ // {
context.RegisterInternedString(str, streamPosition); // context.RegisterInternedString(str, streamPosition);
} // }
return str; // return str;
} //}
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
private static object ReadInt32Value(ref BinaryDeserializationContext context, Type targetType) private static object ReadInt32Value(ref BinaryDeserializationContext context, Type targetType)
@ -1427,23 +1427,23 @@ public static partial class AcBinaryDeserializer
context.RegisterInternedString(str, streamPosition); context.RegisterInternedString(str, streamPosition);
} }
/// <summary> ///// <summary>
/// Skip a string but still register it in the intern table if it meets the length threshold. ///// Skip a string but still register it in the intern table if it meets the length threshold.
/// </summary> ///// </summary>
/// <param name="context">Deserialization context</param> ///// <param name="context">Deserialization context</param>
/// <param name="streamPosition">Position before the type code was read</param> ///// <param name="streamPosition">Position before the type code was read</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)] //[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void SkipAndInternString(ref BinaryDeserializationContext context, int streamPosition) //private static void SkipAndInternString(ref BinaryDeserializationContext context, int streamPosition)
{ //{
var byteLen = (int)context.ReadVarUInt(); // var byteLen = (int)context.ReadVarUInt();
if (byteLen == 0) return; // if (byteLen == 0) return;
var str = context.ReadStringUtf8(byteLen); // var str = context.ReadStringUtf8(byteLen);
if (str.Length >= context.MinStringInternLength) // if (str.Length >= context.MinStringInternLength)
{ // {
context.RegisterInternedString(str, streamPosition); // context.RegisterInternedString(str, streamPosition);
} // }
} //}
private static void SkipObject(ref BinaryDeserializationContext context, BinaryDeserializeTypeMetadata metaData) private static void SkipObject(ref BinaryDeserializationContext context, BinaryDeserializeTypeMetadata metaData)
{ {

View File

@ -125,7 +125,7 @@ public static partial class AcBinarySerializer
// Header // Header
sb.AppendLine("+==============================================================================+"); sb.AppendLine("+==============================================================================+");
sb.AppendLine($"| STRING INTERN ANALYSIS REPORT (Mode: {refMode,-12}) |"); sb.AppendLine($"| STRING INTERN ANALYSIS REPORT (RefMode: {refMode,-12}) |");
sb.AppendLine("+==============================================================================+"); sb.AppendLine("+==============================================================================+");
sb.AppendLine(); sb.AppendLine();