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

View File

@ -237,7 +237,7 @@ public static partial class AcBinarySerializer
/// <summary>
/// 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>
public void WriteInternedStringFooter()
{
@ -261,12 +261,17 @@ public static partial class AcBinarySerializer
// Sort by StreamPosition (ascending) for deserializer sequential check
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++)
{
WriteVarUInt((uint)entries[i].Position);
WriteVarUInt((uint)entries[i].CacheIndex);
dest[i * 2] = entries[i].Position;
dest[i * 2 + 1] = entries[i].CacheIndex;
}
_position += byteCount;
}
#endregion