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

View File

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

View File

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