From d0e26377416eff2d6f601fc075747b6798ca1a8e Mon Sep 17 00:00:00 2001 From: Loretta Date: Tue, 27 Jan 2026 18:49:04 +0100 Subject: [PATCH] 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. --- ...serializer.BinaryDeserializationContext.cs | 55 ++++++++----------- ...rySerializer.BinarySerializationContext.cs | 13 +++-- 2 files changed, 33 insertions(+), 35 deletions(-) diff --git a/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.BinaryDeserializationContext.cs b/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.BinaryDeserializationContext.cs index 2e48d67..88de2d6 100644 --- a/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.BinaryDeserializationContext.cs +++ b/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.BinaryDeserializationContext.cs @@ -20,19 +20,11 @@ public static partial class AcBinaryDeserializer private List? _propertyNames; private Dictionary? _stringCache; - /// - /// Footer entry for position-based string interning. - /// - 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 /// @@ -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 } /// - /// 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. /// 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(); + _dupData = Array.Empty(); _internStringCache = Array.Empty(); _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(_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; } diff --git a/AyCode.Core/Serializers/Binaries/AcBinarySerializer.BinarySerializationContext.cs b/AyCode.Core/Serializers/Binaries/AcBinarySerializer.BinarySerializationContext.cs index 66034e6..ef5fd5a 100644 --- a/AyCode.Core/Serializers/Binaries/AcBinarySerializer.BinarySerializationContext.cs +++ b/AyCode.Core/Serializers/Binaries/AcBinarySerializer.BinarySerializationContext.cs @@ -237,7 +237,7 @@ public static partial class AcBinarySerializer /// /// 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. /// 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(_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