Add WireMode for fast/compact binary serialization
Introduces a WireMode enum to select between Compact (VarInt + UTF-8) and Fast (fixed-width + UTF-16) wire formats for binary serialization. Updates AcBinarySerializerOptions to include a WireMode property (default: Fast). Serialization and deserialization logic now conditionally uses fixed-width or variable-length encoding for integers and strings based on the selected mode, enabling a tradeoff between output size and performance.
This commit is contained in:
parent
b244d9219a
commit
98d7a27245
|
|
@ -114,6 +114,22 @@ public enum ReferenceHandlingMode : byte
|
|||
All = 2
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Wire encoding mode for binary serialization.
|
||||
/// </summary>
|
||||
public enum WireMode : byte
|
||||
{
|
||||
/// <summary>
|
||||
/// Compact encoding: VarInt for integers, UTF-8 for strings. Smaller output.
|
||||
/// </summary>
|
||||
Compact = 0,
|
||||
|
||||
/// <summary>
|
||||
/// Fast encoding: fixed-width integers, UTF-16 for strings. Larger output, faster encode/decode.
|
||||
/// </summary>
|
||||
Fast = 1
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Delegate for custom property mapping during cross-type deserialization/population.
|
||||
/// Enables mapping between different class hierarchies or renamed properties.
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ public static partial class AcBinaryDeserializer
|
|||
{
|
||||
private static readonly Encoding Utf8NoBom = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false);
|
||||
|
||||
#region Buffer State — owned by context for zero virtual dispatch
|
||||
#region Buffer State <EFBFBD> owned by context for zero virtual dispatch
|
||||
|
||||
internal byte[] _buffer = null!;
|
||||
internal int _bufferLength;
|
||||
|
|
@ -20,7 +20,7 @@ public static partial class AcBinaryDeserializer
|
|||
|
||||
#endregion
|
||||
|
||||
// String caching state — needed for WASM optimization
|
||||
// String caching state <EFBFBD> needed for WASM optimization
|
||||
// The cache dictionary is owned by context (pooled), passed in at init time.
|
||||
private bool _useStringCaching;
|
||||
private int _maxCachedStringLength;
|
||||
|
|
@ -172,6 +172,16 @@ public static partial class AcBinaryDeserializer
|
|||
return value;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public T ReadRaw<T>() where T : unmanaged
|
||||
{
|
||||
var size = Unsafe.SizeOf<T>();
|
||||
EnsureAvailable(size);
|
||||
var value = Unsafe.ReadUnaligned<T>(ref _buffer[_position]);
|
||||
_position += size;
|
||||
return value;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public int ReadInt32Raw()
|
||||
{
|
||||
|
|
@ -188,6 +198,7 @@ public static partial class AcBinaryDeserializer
|
|||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public int ReadVarInt()
|
||||
{
|
||||
if (FastWire) { return ReadRaw<int>(); }
|
||||
var raw = ReadVarUInt();
|
||||
var value = (int)(raw >> 1) ^ -(int)(raw & 1);
|
||||
return value;
|
||||
|
|
@ -196,6 +207,7 @@ public static partial class AcBinaryDeserializer
|
|||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public uint ReadVarUInt()
|
||||
{
|
||||
if (FastWire) { return ReadRaw<uint>(); }
|
||||
// Fast path: single byte (0-127) - ~70% of cases
|
||||
var b0 = _buffer[_position];
|
||||
if ((b0 & 0x80) == 0)
|
||||
|
|
@ -245,6 +257,7 @@ public static partial class AcBinaryDeserializer
|
|||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public long ReadVarLong()
|
||||
{
|
||||
if (FastWire) { return ReadRaw<long>(); }
|
||||
var raw = ReadVarULong();
|
||||
var value = (long)(raw >> 1) ^ -((long)raw & 1);
|
||||
return value;
|
||||
|
|
@ -253,6 +266,7 @@ public static partial class AcBinaryDeserializer
|
|||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public ulong ReadVarULong()
|
||||
{
|
||||
if (FastWire) { return ReadRaw<ulong>(); }
|
||||
ulong value = 0;
|
||||
var shift = 0;
|
||||
while (true)
|
||||
|
|
@ -277,7 +291,7 @@ public static partial class AcBinaryDeserializer
|
|||
#endregion
|
||||
|
||||
/// <summary>
|
||||
/// Called on first StringInternFirst marker — disables _stringCache because
|
||||
/// Called on first StringInternFirst marker <EFBFBD> disables _stringCache because
|
||||
/// interned strings are resolved via _internCache and plain strings appear only once.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
|
|
@ -311,6 +325,17 @@ public static partial class AcBinaryDeserializer
|
|||
return string.Empty;
|
||||
}
|
||||
|
||||
// FastWire: length is char count, data is UTF-16 (2 bytes per char)
|
||||
if (FastWire)
|
||||
{
|
||||
var byteLen = length * 2;
|
||||
EnsureAvailable(byteLen);
|
||||
var chars = MemoryMarshal.Cast<byte, char>(_buffer.AsSpan(_position, byteLen));
|
||||
var value = new string(chars);
|
||||
_position += byteLen;
|
||||
return value;
|
||||
}
|
||||
|
||||
EnsureAvailable(length);
|
||||
|
||||
// WASM optimization: cache short strings to reduce allocations
|
||||
|
|
@ -319,8 +344,8 @@ public static partial class AcBinaryDeserializer
|
|||
return ReadStringUtf8Cached(length);
|
||||
}
|
||||
|
||||
// ASCII fast path: short strings (?128 bytes) with all ASCII bytes
|
||||
// use string.Create + direct byte?char widening, avoiding UTF8Encoding overhead.
|
||||
// ASCII fast path: short strings (≤128 bytes) with all ASCII bytes
|
||||
// use string.Create + direct byte→char widening, avoiding UTF8Encoding overhead.
|
||||
if (length <= 128 && System.Text.Ascii.IsValid(_buffer.AsSpan(_position, length)))
|
||||
{
|
||||
var pos = _position;
|
||||
|
|
@ -333,9 +358,9 @@ public static partial class AcBinaryDeserializer
|
|||
});
|
||||
}
|
||||
|
||||
var value = Utf8NoBom.GetString(_buffer, _position, length);
|
||||
var value2 = Utf8NoBom.GetString(_buffer, _position, length);
|
||||
_position += length;
|
||||
return value;
|
||||
return value2;
|
||||
}
|
||||
|
||||
private string ReadStringUtf8Cached(int length)
|
||||
|
|
@ -366,7 +391,7 @@ public static partial class AcBinaryDeserializer
|
|||
|
||||
/// <summary>
|
||||
/// Full-content hash for string caching.
|
||||
/// CRITICAL: DO NOT SIMPLIFY — prevents hash collisions for similar property names.
|
||||
/// CRITICAL: DO NOT SIMPLIFY <EFBFBD> prevents hash collisions for similar property names.
|
||||
/// See BinaryDeserializationContext for full history.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
|
|
|
|||
|
|
@ -63,6 +63,7 @@ public static partial class AcBinaryDeserializer
|
|||
public bool HasMetadata;
|
||||
public bool IsMergeMode;
|
||||
public bool RemoveOrphanedItems;
|
||||
public bool FastWire;
|
||||
|
||||
// Options-derived properties
|
||||
public byte MinStringInternLength => Options.MinStringInternLength;
|
||||
|
|
@ -138,6 +139,7 @@ public static partial class AcBinaryDeserializer
|
|||
HasMetadata = false;
|
||||
IsMergeMode = false;
|
||||
RemoveOrphanedItems = false;
|
||||
FastWire = Options.WireMode == WireMode.Fast;
|
||||
ChainTracker = null;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -227,6 +227,7 @@ public static partial class AcBinarySerializer
|
|||
/// Reference handling is safe because generated code inlines TryConsumeWritePlanEntry for IId types.
|
||||
/// </summary>
|
||||
public bool IsDirectObjectWrite => !UseMetadata && !HasPropertyFilter;
|
||||
public bool FastWire { get; private set; }
|
||||
public byte MinStringInternLength => Options.MinStringInternLength;
|
||||
public byte MaxStringInternLength => Options.MaxStringInternLength;
|
||||
public BinaryPropertyFilter? PropertyFilter => Options.PropertyFilter;
|
||||
|
|
@ -249,6 +250,7 @@ public static partial class AcBinarySerializer
|
|||
public BinarySerializationContext(AcBinarySerializerOptions options)
|
||||
{
|
||||
Reset(options);
|
||||
FastWire = options.WireMode == WireMode.Fast;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -262,6 +264,7 @@ public static partial class AcBinarySerializer
|
|||
// IMPORTANT: base.Reset sets Options first, so derived code can use Options-derived properties
|
||||
base.Reset(options);
|
||||
HasPropertyFilter = Options.PropertyFilter != null;
|
||||
FastWire = Options.WireMode == WireMode.Fast;
|
||||
}
|
||||
|
||||
public override void Clear()
|
||||
|
|
@ -386,6 +389,7 @@ public static partial class AcBinarySerializer
|
|||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void WriteVarUInt(uint value)
|
||||
{
|
||||
if (FastWire) { WriteRaw(value); return; }
|
||||
if (value < 0x80)
|
||||
{
|
||||
if (_position >= _bufferEnd)
|
||||
|
|
@ -405,6 +409,7 @@ public static partial class AcBinarySerializer
|
|||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void WriteVarInt(int value)
|
||||
{
|
||||
if (FastWire) { WriteRaw(value); return; }
|
||||
var encoded = (uint)((value << 1) ^ (value >> 31));
|
||||
WriteVarUInt(encoded);
|
||||
}
|
||||
|
|
@ -412,6 +417,7 @@ public static partial class AcBinarySerializer
|
|||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void WriteVarULong(ulong value)
|
||||
{
|
||||
if (FastWire) { WriteRaw(value); return; }
|
||||
if (value < 0x80)
|
||||
{
|
||||
if (_position >= _bufferEnd)
|
||||
|
|
@ -431,6 +437,7 @@ public static partial class AcBinarySerializer
|
|||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void WriteVarLong(long value)
|
||||
{
|
||||
if (FastWire) { WriteRaw(value); return; }
|
||||
var encoded = (ulong)((value << 1) ^ (value >> 63));
|
||||
WriteVarULong(encoded);
|
||||
}
|
||||
|
|
@ -481,6 +488,18 @@ public static partial class AcBinarySerializer
|
|||
|
||||
public void WriteStringUtf8(string value)
|
||||
{
|
||||
if (FastWire)
|
||||
{
|
||||
// UTF-16: char count (fixed uint) + raw char data (zero-encoding memcopy)
|
||||
var charLen = value.Length;
|
||||
var byteLen = charLen * 2;
|
||||
WriteRaw(charLen);
|
||||
EnsureCapacity(byteLen);
|
||||
MemoryMarshal.AsBytes(value.AsSpan()).CopyTo(_buffer.AsSpan(_position, byteLen));
|
||||
_position += byteLen;
|
||||
return;
|
||||
}
|
||||
|
||||
var charLength = value.Length;
|
||||
|
||||
// Speculative ASCII fast path: assume byteCount == charLength
|
||||
|
|
|
|||
|
|
@ -970,6 +970,14 @@ public static partial class AcBinarySerializer
|
|||
#endif
|
||||
}
|
||||
|
||||
// FastWire: skip FixStr optimization (UTF-8 specific), write String marker + UTF-16 data
|
||||
if (context.FastWire)
|
||||
{
|
||||
context.WriteByte(BinaryTypeCode.String);
|
||||
context.WriteStringUtf8(value);
|
||||
return;
|
||||
}
|
||||
|
||||
// Fast path for short strings: check length first (cheap), then ASCII
|
||||
// FixStr encodes type+length in single byte for strings <= 31 chars
|
||||
var length = value.Length;
|
||||
|
|
|
|||
|
|
@ -95,6 +95,13 @@ public sealed class AcBinarySerializerOptions : AcSerializerOptions
|
|||
/// </summary>
|
||||
public StringInterningMode UseStringInterning { get; set; } = StringInterningMode.Attribute;
|
||||
|
||||
/// <summary>
|
||||
/// Wire encoding mode.
|
||||
/// Compact: VarInt + UTF-8 (default, smaller output).
|
||||
/// Fast: Fixed-width integers + UTF-16 (larger output, faster encode/decode).
|
||||
/// </summary>
|
||||
public WireMode WireMode { get; set; } = WireMode.Fast;
|
||||
|
||||
/// <summary>
|
||||
/// When true, checks for duplicate property name hashes during serialization (UseMetadata mode).
|
||||
/// Throws exception if FNV-1a hash collision is detected between property names of the same type.
|
||||
|
|
|
|||
Loading…
Reference in New Issue