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
|
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>
|
/// <summary>
|
||||||
/// Delegate for custom property mapping during cross-type deserialization/population.
|
/// Delegate for custom property mapping during cross-type deserialization/population.
|
||||||
/// Enables mapping between different class hierarchies or renamed properties.
|
/// 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);
|
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 byte[] _buffer = null!;
|
||||||
internal int _bufferLength;
|
internal int _bufferLength;
|
||||||
|
|
@ -20,7 +20,7 @@ public static partial class AcBinaryDeserializer
|
||||||
|
|
||||||
#endregion
|
#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.
|
// The cache dictionary is owned by context (pooled), passed in at init time.
|
||||||
private bool _useStringCaching;
|
private bool _useStringCaching;
|
||||||
private int _maxCachedStringLength;
|
private int _maxCachedStringLength;
|
||||||
|
|
@ -172,6 +172,16 @@ public static partial class AcBinaryDeserializer
|
||||||
return value;
|
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)]
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
public int ReadInt32Raw()
|
public int ReadInt32Raw()
|
||||||
{
|
{
|
||||||
|
|
@ -188,6 +198,7 @@ public static partial class AcBinaryDeserializer
|
||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
public int ReadVarInt()
|
public int ReadVarInt()
|
||||||
{
|
{
|
||||||
|
if (FastWire) { return ReadRaw<int>(); }
|
||||||
var raw = ReadVarUInt();
|
var raw = ReadVarUInt();
|
||||||
var value = (int)(raw >> 1) ^ -(int)(raw & 1);
|
var value = (int)(raw >> 1) ^ -(int)(raw & 1);
|
||||||
return value;
|
return value;
|
||||||
|
|
@ -196,6 +207,7 @@ public static partial class AcBinaryDeserializer
|
||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
public uint ReadVarUInt()
|
public uint ReadVarUInt()
|
||||||
{
|
{
|
||||||
|
if (FastWire) { return ReadRaw<uint>(); }
|
||||||
// Fast path: single byte (0-127) - ~70% of cases
|
// Fast path: single byte (0-127) - ~70% of cases
|
||||||
var b0 = _buffer[_position];
|
var b0 = _buffer[_position];
|
||||||
if ((b0 & 0x80) == 0)
|
if ((b0 & 0x80) == 0)
|
||||||
|
|
@ -245,6 +257,7 @@ public static partial class AcBinaryDeserializer
|
||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
public long ReadVarLong()
|
public long ReadVarLong()
|
||||||
{
|
{
|
||||||
|
if (FastWire) { return ReadRaw<long>(); }
|
||||||
var raw = ReadVarULong();
|
var raw = ReadVarULong();
|
||||||
var value = (long)(raw >> 1) ^ -((long)raw & 1);
|
var value = (long)(raw >> 1) ^ -((long)raw & 1);
|
||||||
return value;
|
return value;
|
||||||
|
|
@ -253,6 +266,7 @@ public static partial class AcBinaryDeserializer
|
||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
public ulong ReadVarULong()
|
public ulong ReadVarULong()
|
||||||
{
|
{
|
||||||
|
if (FastWire) { return ReadRaw<ulong>(); }
|
||||||
ulong value = 0;
|
ulong value = 0;
|
||||||
var shift = 0;
|
var shift = 0;
|
||||||
while (true)
|
while (true)
|
||||||
|
|
@ -277,7 +291,7 @@ public static partial class AcBinaryDeserializer
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
/// <summary>
|
/// <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.
|
/// interned strings are resolved via _internCache and plain strings appear only once.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
|
@ -311,6 +325,17 @@ public static partial class AcBinaryDeserializer
|
||||||
return string.Empty;
|
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);
|
EnsureAvailable(length);
|
||||||
|
|
||||||
// WASM optimization: cache short strings to reduce allocations
|
// WASM optimization: cache short strings to reduce allocations
|
||||||
|
|
@ -319,8 +344,8 @@ public static partial class AcBinaryDeserializer
|
||||||
return ReadStringUtf8Cached(length);
|
return ReadStringUtf8Cached(length);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ASCII fast path: short strings (?128 bytes) with all ASCII bytes
|
// ASCII fast path: short strings (≤128 bytes) with all ASCII bytes
|
||||||
// use string.Create + direct byte?char widening, avoiding UTF8Encoding overhead.
|
// use string.Create + direct byte→char widening, avoiding UTF8Encoding overhead.
|
||||||
if (length <= 128 && System.Text.Ascii.IsValid(_buffer.AsSpan(_position, length)))
|
if (length <= 128 && System.Text.Ascii.IsValid(_buffer.AsSpan(_position, length)))
|
||||||
{
|
{
|
||||||
var pos = _position;
|
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;
|
_position += length;
|
||||||
return value;
|
return value2;
|
||||||
}
|
}
|
||||||
|
|
||||||
private string ReadStringUtf8Cached(int length)
|
private string ReadStringUtf8Cached(int length)
|
||||||
|
|
@ -366,7 +391,7 @@ public static partial class AcBinaryDeserializer
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Full-content hash for string caching.
|
/// 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.
|
/// See BinaryDeserializationContext for full history.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
|
|
||||||
|
|
@ -63,6 +63,7 @@ public static partial class AcBinaryDeserializer
|
||||||
public bool HasMetadata;
|
public bool HasMetadata;
|
||||||
public bool IsMergeMode;
|
public bool IsMergeMode;
|
||||||
public bool RemoveOrphanedItems;
|
public bool RemoveOrphanedItems;
|
||||||
|
public bool FastWire;
|
||||||
|
|
||||||
// Options-derived properties
|
// Options-derived properties
|
||||||
public byte MinStringInternLength => Options.MinStringInternLength;
|
public byte MinStringInternLength => Options.MinStringInternLength;
|
||||||
|
|
@ -138,6 +139,7 @@ public static partial class AcBinaryDeserializer
|
||||||
HasMetadata = false;
|
HasMetadata = false;
|
||||||
IsMergeMode = false;
|
IsMergeMode = false;
|
||||||
RemoveOrphanedItems = false;
|
RemoveOrphanedItems = false;
|
||||||
|
FastWire = Options.WireMode == WireMode.Fast;
|
||||||
ChainTracker = null;
|
ChainTracker = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -227,6 +227,7 @@ public static partial class AcBinarySerializer
|
||||||
/// Reference handling is safe because generated code inlines TryConsumeWritePlanEntry for IId types.
|
/// Reference handling is safe because generated code inlines TryConsumeWritePlanEntry for IId types.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool IsDirectObjectWrite => !UseMetadata && !HasPropertyFilter;
|
public bool IsDirectObjectWrite => !UseMetadata && !HasPropertyFilter;
|
||||||
|
public bool FastWire { get; private set; }
|
||||||
public byte MinStringInternLength => Options.MinStringInternLength;
|
public byte MinStringInternLength => Options.MinStringInternLength;
|
||||||
public byte MaxStringInternLength => Options.MaxStringInternLength;
|
public byte MaxStringInternLength => Options.MaxStringInternLength;
|
||||||
public BinaryPropertyFilter? PropertyFilter => Options.PropertyFilter;
|
public BinaryPropertyFilter? PropertyFilter => Options.PropertyFilter;
|
||||||
|
|
@ -249,6 +250,7 @@ public static partial class AcBinarySerializer
|
||||||
public BinarySerializationContext(AcBinarySerializerOptions options)
|
public BinarySerializationContext(AcBinarySerializerOptions options)
|
||||||
{
|
{
|
||||||
Reset(options);
|
Reset(options);
|
||||||
|
FastWire = options.WireMode == WireMode.Fast;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -262,6 +264,7 @@ public static partial class AcBinarySerializer
|
||||||
// IMPORTANT: base.Reset sets Options first, so derived code can use Options-derived properties
|
// IMPORTANT: base.Reset sets Options first, so derived code can use Options-derived properties
|
||||||
base.Reset(options);
|
base.Reset(options);
|
||||||
HasPropertyFilter = Options.PropertyFilter != null;
|
HasPropertyFilter = Options.PropertyFilter != null;
|
||||||
|
FastWire = Options.WireMode == WireMode.Fast;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void Clear()
|
public override void Clear()
|
||||||
|
|
@ -386,6 +389,7 @@ public static partial class AcBinarySerializer
|
||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
public void WriteVarUInt(uint value)
|
public void WriteVarUInt(uint value)
|
||||||
{
|
{
|
||||||
|
if (FastWire) { WriteRaw(value); return; }
|
||||||
if (value < 0x80)
|
if (value < 0x80)
|
||||||
{
|
{
|
||||||
if (_position >= _bufferEnd)
|
if (_position >= _bufferEnd)
|
||||||
|
|
@ -405,6 +409,7 @@ public static partial class AcBinarySerializer
|
||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
public void WriteVarInt(int value)
|
public void WriteVarInt(int value)
|
||||||
{
|
{
|
||||||
|
if (FastWire) { WriteRaw(value); return; }
|
||||||
var encoded = (uint)((value << 1) ^ (value >> 31));
|
var encoded = (uint)((value << 1) ^ (value >> 31));
|
||||||
WriteVarUInt(encoded);
|
WriteVarUInt(encoded);
|
||||||
}
|
}
|
||||||
|
|
@ -412,6 +417,7 @@ public static partial class AcBinarySerializer
|
||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
public void WriteVarULong(ulong value)
|
public void WriteVarULong(ulong value)
|
||||||
{
|
{
|
||||||
|
if (FastWire) { WriteRaw(value); return; }
|
||||||
if (value < 0x80)
|
if (value < 0x80)
|
||||||
{
|
{
|
||||||
if (_position >= _bufferEnd)
|
if (_position >= _bufferEnd)
|
||||||
|
|
@ -431,6 +437,7 @@ public static partial class AcBinarySerializer
|
||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
public void WriteVarLong(long value)
|
public void WriteVarLong(long value)
|
||||||
{
|
{
|
||||||
|
if (FastWire) { WriteRaw(value); return; }
|
||||||
var encoded = (ulong)((value << 1) ^ (value >> 63));
|
var encoded = (ulong)((value << 1) ^ (value >> 63));
|
||||||
WriteVarULong(encoded);
|
WriteVarULong(encoded);
|
||||||
}
|
}
|
||||||
|
|
@ -481,6 +488,18 @@ public static partial class AcBinarySerializer
|
||||||
|
|
||||||
public void WriteStringUtf8(string value)
|
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;
|
var charLength = value.Length;
|
||||||
|
|
||||||
// Speculative ASCII fast path: assume byteCount == charLength
|
// Speculative ASCII fast path: assume byteCount == charLength
|
||||||
|
|
|
||||||
|
|
@ -970,6 +970,14 @@ public static partial class AcBinarySerializer
|
||||||
#endif
|
#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
|
// Fast path for short strings: check length first (cheap), then ASCII
|
||||||
// FixStr encodes type+length in single byte for strings <= 31 chars
|
// FixStr encodes type+length in single byte for strings <= 31 chars
|
||||||
var length = value.Length;
|
var length = value.Length;
|
||||||
|
|
|
||||||
|
|
@ -95,6 +95,13 @@ public sealed class AcBinarySerializerOptions : AcSerializerOptions
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public StringInterningMode UseStringInterning { get; set; } = StringInterningMode.Attribute;
|
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>
|
/// <summary>
|
||||||
/// When true, checks for duplicate property name hashes during serialization (UseMetadata mode).
|
/// 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.
|
/// Throws exception if FNV-1a hash collision is detected between property names of the same type.
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue