Refactor string interning to use flat int[] for perf

Replaces DupEntry[] with flat int[] for position-based string interning in AcBinaryDeserializer and AcBinarySerializer. Serializer now writes (position, cacheIndex) pairs as fixed int32s in bulk, and deserializer reads them with MemoryMarshal.Cast for ultra-fast, cache-friendly access. This eliminates per-pair parsing overhead and streamlines the hot path for string interning.
This commit is contained in:
Loretta 2026-01-27 18:49:04 +01:00
parent f313d5d9ea
commit d0e2637741
2 changed files with 33 additions and 35 deletions

View File

@ -20,19 +20,11 @@ public static partial class AcBinaryDeserializer
private List<string>? _propertyNames; private List<string>? _propertyNames;
private Dictionary<int, string>? _stringCache; private Dictionary<int, string>? _stringCache;
/// <summary> // Position-based string interning: flat int[] for cache-friendly access
/// Footer entry for position-based string interning. // Layout: [pos0, cacheIdx0, pos1, cacheIdx1, ...] - pairs sorted by position
/// </summary> private int[]? _dupData; // Footer data: (position, cacheIndex) pairs as flat int array
private struct DupEntry
{
public int Position; // Stream position where string was first written
public int CacheIndex; // Index in _internStringCache
}
// Position-based string interning: 100% reliable cache matching
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 index in _dupData (increments by 2)
private int _nextDupPosition; // Cached next dup position - avoids array access in hot path private int _nextDupPosition; // Cached next dup position - avoids array access in hot path
/// <summary> /// <summary>
@ -85,7 +77,7 @@ public static partial class AcBinaryDeserializer
_stringCache = null; _stringCache = null;
// Position-based string interning fields // Position-based string interning fields
_dupEntries = null; _dupData = null;
_internStringCache = null; _internStringCache = null;
_dupCheckIndex = 0; _dupCheckIndex = 0;
_nextDupPosition = int.MaxValue; _nextDupPosition = int.MaxValue;
@ -176,8 +168,8 @@ public static partial class AcBinaryDeserializer
} }
/// <summary> /// <summary>
/// Reads string intern footer: [dupCount][(position, cacheIndex), ...] /// Reads string intern footer: [dupCount][pos0][idx0][pos1][idx1]...
/// Position-based format for 100% reliable cache matching. /// Fixed int32 format for ultra-fast MemoryMarshal.Cast bulk read.
/// </summary> /// </summary>
private void ReadFooterStringIndices(int footerPosition) private void ReadFooterStringIndices(int footerPosition)
{ {
@ -187,28 +179,28 @@ public static partial class AcBinaryDeserializer
// Seek to footer // Seek to footer
_position = footerPosition; _position = footerPosition;
// Read dup count and (position, cacheIndex) pairs // Read dup count (still VarUInt for backward compat header)
var dupCount = (int)ReadVarUInt(); var dupCount = (int)ReadVarUInt();
if (dupCount == 0) if (dupCount == 0)
{ {
_dupEntries = Array.Empty<DupEntry>(); _dupData = Array.Empty<int>();
_internStringCache = Array.Empty<string>(); _internStringCache = Array.Empty<string>();
_nextDupPosition = int.MaxValue; _nextDupPosition = int.MaxValue;
} }
else else
{ {
_dupEntries = new DupEntry[dupCount]; // Bulk read: dupCount * 2 int32s (position, cacheIndex pairs)
for (var i = 0; i < dupCount; i++) var intCount = dupCount * 2;
{ var byteCount = intCount * sizeof(int);
var position = (int)ReadVarUInt(); EnsureAvailable(byteCount);
var cacheIndex = (int)ReadVarUInt();
_dupEntries[i] = new DupEntry { Position = position, CacheIndex = cacheIndex }; _dupData = new int[intCount];
} MemoryMarshal.Cast<byte, int>(_buffer.Slice(_position, byteCount)).CopyTo(_dupData);
_position += byteCount;
// 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 // Cache first dup position for ultra-fast hot path
_nextDupPosition = _dupEntries[0].Position; _nextDupPosition = _dupData[0];
} }
// Seek back to data position // Seek back to data position
@ -577,14 +569,15 @@ public static partial class AcBinaryDeserializer
return; return;
// Match! Store in cache and advance to next dup position // Match! Store in cache and advance to next dup position
var entries = _dupEntries!; // _dupData layout: [pos0, cacheIdx0, pos1, cacheIdx1, ...]
var data = _dupData!;
var idx = _dupCheckIndex; var idx = _dupCheckIndex;
_internStringCache![entries[idx].CacheIndex] = value; _internStringCache![data[idx + 1]] = value; // cacheIndex is at odd positions
idx++; idx += 2;
_dupCheckIndex = idx; _dupCheckIndex = idx;
_nextDupPosition = idx < entries.Length _nextDupPosition = idx < data.Length
? entries[idx].Position ? data[idx] // next position is at even index
: int.MaxValue; : int.MaxValue;
} }

View File

@ -237,7 +237,7 @@ public static partial class AcBinarySerializer
/// <summary> /// <summary>
/// Writes the footer with (position, cacheIndex) pairs sorted by position. /// Writes the footer with (position, cacheIndex) pairs sorted by position.
/// Position-based approach ensures 100% reliable cache matching in deserializer. /// Fixed int32 format for ultra-fast MemoryMarshal.Cast bulk read in deserializer.
/// </summary> /// </summary>
public void WriteInternedStringFooter() public void WriteInternedStringFooter()
{ {
@ -261,12 +261,17 @@ public static partial class AcBinarySerializer
// Sort by StreamPosition (ascending) for deserializer sequential check // Sort by StreamPosition (ascending) for deserializer sequential check
entries.Sort((a, b) => a.Position.CompareTo(b.Position)); entries.Sort((a, b) => a.Position.CompareTo(b.Position));
// Write pairs: (position, cacheIndex) // Write pairs as fixed int32s: [pos0][idx0][pos1][idx1]...
// This allows MemoryMarshal.Cast bulk read in deserializer
var byteCount = _nextCacheIndex * 2 * sizeof(int);
EnsureCapacity(byteCount);
var dest = MemoryMarshal.Cast<byte, int>(_buffer.AsSpan(_position, byteCount));
for (var i = 0; i < _nextCacheIndex; i++) for (var i = 0; i < _nextCacheIndex; i++)
{ {
WriteVarUInt((uint)entries[i].Position); dest[i * 2] = entries[i].Position;
WriteVarUInt((uint)entries[i].CacheIndex); dest[i * 2 + 1] = entries[i].CacheIndex;
} }
_position += byteCount;
} }
#endregion #endregion