Refactor: make AcBinarySerializer fully generic on output
Major internal refactor: AcBinarySerializer and BinarySerializationContext are now generic on TOutput : BinaryOutputBase, enabling JIT devirtualization and eliminating virtual dispatch in hot serialization loops. All serialization logic (WriteValue, WriteObject, WriteArray, etc.) is now generic on TOutput and delegates buffer operations to the output instance (ArrayBinaryOutput or BufferWriterBinaryOutput). Context pooling is now per output type. All buffer management is moved to output classes. The public API is unchanged, but the internal architecture is now fully generic and ready for further JIT optimizations. Also disables the source generator and sets UseMetadata=false by default.
This commit is contained in:
parent
0bde311aa1
commit
270f1b8265
|
|
@ -31,7 +31,8 @@
|
|||
"Bash(Remove-Item \"H:\\\\Applications\\\\Aycode\\\\Source\\\\AyCode.Core\\\\AyCode.Core\\\\Serializers\\\\Toons\\\\AcToonSerializer.RelationshipDetection.cs\")",
|
||||
"Bash(find:*)",
|
||||
"Bash(dir:*)",
|
||||
"Bash(git stash:*)"
|
||||
"Bash(git stash:*)",
|
||||
"WebFetch(domain:github.com)"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -51,6 +51,9 @@ public static class Program
|
|||
#else
|
||||
private static int WarmupIterations = 2000;
|
||||
private static int TestIterations = 1000;
|
||||
|
||||
//private static int WarmupIterations = 5000;
|
||||
//private static int TestIterations = 2000;
|
||||
#endif
|
||||
|
||||
public static void Main(string[] args)
|
||||
|
|
@ -209,11 +212,16 @@ public static class Program
|
|||
{
|
||||
|
||||
// AcBinary variants
|
||||
new AcBinaryBenchmark(testData.Order, AcBinarySerializerOptions.Default, SerializerAcBinaryDefault),
|
||||
new AcBinaryBenchmark(testData.Order, AcBinarySerializerOptions.WithoutReferenceHandling, SerializerAcBinaryNoRef),
|
||||
//new AcBinaryBenchmark(testData.Order, AcBinarySerializerOptions.Default, SerializerAcBinaryDefault),
|
||||
//new AcBinaryBenchmark(testData.Order, AcBinarySerializerOptions.WithoutReferenceHandling, SerializerAcBinaryNoRef),
|
||||
//new AcBinaryBenchmark(testData.Order, AcBinarySerializerOptions.FastMode, SerializerAcBinaryFastMode),
|
||||
//new AcBinaryBenchmark(testData.Order, new AcBinarySerializerOptions { UseStringInterning = StringInterningMode.None }, SerializerAcBinaryNoIntern),
|
||||
|
||||
new AcBinaryBenchmark(testData.Order, AcBinarySerializerOptions.FastMode, SerializerAcBinaryDefault),
|
||||
new AcBinaryBenchmark(testData.Order, AcBinarySerializerOptions.FastMode, SerializerAcBinaryNoRef),
|
||||
new AcBinaryBenchmark(testData.Order, AcBinarySerializerOptions.FastMode, SerializerAcBinaryFastMode),
|
||||
new AcBinaryBenchmark(testData.Order, new AcBinarySerializerOptions { UseStringInterning = StringInterningMode.None }, SerializerAcBinaryNoIntern),
|
||||
|
||||
new AcBinaryBenchmark(testData.Order, AcBinarySerializerOptions.FastMode, SerializerAcBinaryNoIntern),
|
||||
|
||||
// AcJson
|
||||
new AcJsonBenchmark(testData.Order, AcJsonSerializerOptions.Default, SerializerAcJsonDefault),
|
||||
|
||||
|
|
@ -550,8 +558,8 @@ public static class Program
|
|||
}
|
||||
|
||||
System.Console.WriteLine($"└{"─".PadRight(6, '─')}─{"─".PadRight(27, '─')}┴{"─".PadRight(12, '─')}┴{"─".PadRight(14, '─')}┴{"─".PadRight(14, '─')}┴{"─".PadRight(13, '─')}┘");
|
||||
System.Console.WriteLine($"GrowBufferCount: {AcBinarySerializer.GrowBufferCount}");
|
||||
System.Console.WriteLine($"GrowBufferTotalBytes: {AcBinarySerializer.GrowBufferTotalBytes:N0} bytes");
|
||||
//System.Console.WriteLine($"GrowBufferCount: {AcBinarySerializer.GrowBufferCount}");
|
||||
//System.Console.WriteLine($"GrowBufferTotalBytes: {AcBinarySerializer.GrowBufferTotalBytes:N0} bytes");
|
||||
}
|
||||
|
||||
// Summary: Best serializer for each category
|
||||
|
|
@ -751,8 +759,8 @@ public static class Program
|
|||
sb.AppendLine($" {SerializerAcBinaryDefault} vs {SerializerMessagePack}: Size {sizePct:+0;-0}% │ Ser {serPct:+0;-0}% │ Des {desPct:+0;-0}% │ RT {rtPct:+0;-0}%");
|
||||
}
|
||||
|
||||
sb.AppendLine($"GrowBufferCount: {AcBinarySerializer.GrowBufferCount}");
|
||||
sb.AppendLine($"GrowBufferTotalBytes: {AcBinarySerializer.GrowBufferTotalBytes:N0} bytes");
|
||||
//sb.AppendLine($"GrowBufferCount: {AcBinarySerializer.GrowBufferCount}");
|
||||
//sb.AppendLine($"GrowBufferTotalBytes: {AcBinarySerializer.GrowBufferTotalBytes:N0} bytes");
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -41,7 +41,7 @@ public static partial class AcBinaryDeserializer
|
|||
}
|
||||
|
||||
// Fast check: only look for generated serializer if type has [AcBinarySerializable] attribute
|
||||
if (type.IsDefined(typeof(AcBinarySerializableAttribute), inherit: false))
|
||||
if (false && type.IsDefined(typeof(AcBinarySerializableAttribute), inherit: false))
|
||||
{
|
||||
GeneratedSerializerType = FindGeneratedSerializerType(type);
|
||||
HasGeneratedDeserializer = GeneratedSerializerType != null;
|
||||
|
|
|
|||
|
|
@ -2,11 +2,7 @@ using System;
|
|||
using System.Buffers;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Numerics;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Runtime.Intrinsics;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using static AyCode.Core.Helpers.JsonUtilities;
|
||||
|
||||
|
|
@ -14,12 +10,12 @@ namespace AyCode.Core.Serializers.Binaries;
|
|||
|
||||
public static partial class AcBinarySerializer
|
||||
{
|
||||
private static class BinarySerializationContextPool
|
||||
private static class BinarySerializationContextPool<TOutput> where TOutput : BinaryOutputBase
|
||||
{
|
||||
private static readonly ConcurrentQueue<BinarySerializationContext> Pool = new();
|
||||
private static readonly ConcurrentQueue<BinarySerializationContext<TOutput>> Pool = new();
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static BinarySerializationContext Get(AcBinarySerializerOptions options)
|
||||
public static BinarySerializationContext<TOutput> Get(AcBinarySerializerOptions options)
|
||||
{
|
||||
if (Pool.TryDequeue(out var context))
|
||||
{
|
||||
|
|
@ -27,17 +23,17 @@ public static partial class AcBinarySerializer
|
|||
return context;
|
||||
}
|
||||
|
||||
return new BinarySerializationContext(options);
|
||||
return new BinarySerializationContext<TOutput>(options);
|
||||
}
|
||||
|
||||
public static void ReturnAsync(BinarySerializationContext context)
|
||||
public static void ReturnAsync(BinarySerializationContext<TOutput> context)
|
||||
{
|
||||
// 🔥 FIRE-AND-FORGET: cleanup háttérben
|
||||
ThreadPool.UnsafeQueueUserWorkItem(Return, context, preferLocal: true);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void Return(BinarySerializationContext context)
|
||||
public static void Return(BinarySerializationContext<TOutput> context)
|
||||
{
|
||||
if (Pool.Count < context.Options.MaxContextPoolSize)
|
||||
{
|
||||
|
|
@ -51,50 +47,23 @@ public static partial class AcBinarySerializer
|
|||
}
|
||||
}
|
||||
|
||||
public static int GrowBufferCount =>
|
||||
#if DEBUG
|
||||
BinarySerializationContext.GrowBufferCount;
|
||||
#else
|
||||
-1;
|
||||
#endif
|
||||
|
||||
public static long GrowBufferTotalBytes =>
|
||||
#if DEBUG
|
||||
BinarySerializationContext.GrowBufferTotalBytes;
|
||||
#else
|
||||
-1;
|
||||
#endif
|
||||
|
||||
/// <summary>
|
||||
/// Binary serialization context. Public for generated serializers.
|
||||
/// Binary serialization context. Generic on TOutput for JIT devirtualization.
|
||||
/// TOutput is the binary output target (ArrayBinaryOutput for byte[], BufferWriterBinaryOutput for IBufferWriter).
|
||||
/// All write operations are delegated to Output — the context only manages serialization state
|
||||
/// (string interning, reference tracking, metadata, property filtering).
|
||||
/// </summary>
|
||||
internal sealed class BinarySerializationContext : SerializationContextBase<BinarySerializeTypeMetadata, AcBinarySerializerOptions>, IDisposable
|
||||
internal sealed class BinarySerializationContext<TOutput>
|
||||
: SerializationContextBase<BinarySerializeTypeMetadata, AcBinarySerializerOptions>, IDisposable
|
||||
where TOutput : BinaryOutputBase
|
||||
{
|
||||
private const int MinBufferSize = 512;
|
||||
private const int BufferHalvingThreshold = 4; // Halve buffer when > _initialBufferSize * this
|
||||
private const int PropertyIndexBufferMaxCache = 512;
|
||||
private const int PropertyStateBufferMaxCache = 512;
|
||||
private const int InitialInternCapacity = 32;
|
||||
private byte[] _buffer;
|
||||
private int _position;
|
||||
private int _initialBufferSize;
|
||||
|
||||
#if DEBUG
|
||||
/// <summary>
|
||||
/// Counts how many times GrowBuffer was called during serialization.
|
||||
/// Used for benchmarking buffer allocation efficiency.
|
||||
/// </summary>
|
||||
public static int GrowBufferCount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Total bytes allocated by GrowBuffer during serialization.
|
||||
/// Used for benchmarking buffer allocation efficiency.
|
||||
/// Strongly-typed output target for JIT devirtualization in the write pass.
|
||||
/// </summary>
|
||||
public static long GrowBufferTotalBytes { get; set; }
|
||||
#endif
|
||||
|
||||
// Use shared reference tracker from AcSerializerCommon
|
||||
//private readonly AcSerializerCommon.SerializationReferenceTracker _refTracker = new();
|
||||
public TOutput Output = default!;
|
||||
|
||||
private IdentityMap<string, InternEntry>? _stringInternMap;
|
||||
private int _nextCacheIndex; // Next dense cache index to assign (starts at 0, uses ++_nextCacheIndex)
|
||||
|
|
@ -160,12 +129,17 @@ public static partial class AcBinarySerializer
|
|||
/// </summary>
|
||||
public bool HasPropertyFilter { get; private set; }
|
||||
|
||||
public int Position => _position;
|
||||
/// <summary>
|
||||
/// Current output position (delegates to Output).
|
||||
/// </summary>
|
||||
public int Position
|
||||
{
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
get => Output.Position;
|
||||
}
|
||||
|
||||
public BinarySerializationContext(AcBinarySerializerOptions options)
|
||||
{
|
||||
_initialBufferSize = Math.Max(options.InitialBufferCapacity, MinBufferSize);
|
||||
_buffer = ArrayPool<byte>.Shared.Rent(_initialBufferSize);
|
||||
Reset(options);
|
||||
}
|
||||
|
||||
|
|
@ -179,26 +153,11 @@ public static partial class AcBinarySerializer
|
|||
{
|
||||
// IMPORTANT: base.Reset sets Options first, so derived code can use Options-derived properties
|
||||
base.Reset(options);
|
||||
|
||||
_position = 0;
|
||||
_initialBufferSize = Math.Max(Options.InitialBufferCapacity, MinBufferSize);
|
||||
HasPropertyFilter = Options.PropertyFilter != null;
|
||||
|
||||
// NOTE: GrowBufferCount és GrowBufferTotalBytes NEM nullázódik itt!
|
||||
// Kumulatívan gyűjtjük a benchmark során.
|
||||
|
||||
if (_buffer.Length < _initialBufferSize || _buffer.Length > _initialBufferSize * BufferHalvingThreshold)
|
||||
{
|
||||
ArrayPool<byte>.Shared.Return(_buffer);
|
||||
_buffer = ArrayPool<byte>.Shared.Rent(_initialBufferSize);
|
||||
}
|
||||
}
|
||||
|
||||
public override void Clear()
|
||||
{
|
||||
_position = 0;
|
||||
|
||||
//_refTracker.Reset();
|
||||
_stringInternMap?.Reset();
|
||||
_nextCacheIndex = 0;
|
||||
_nextFirstIndex = 0;
|
||||
|
|
@ -215,27 +174,12 @@ public static partial class AcBinarySerializer
|
|||
_propertyStateBuffer = null;
|
||||
}
|
||||
|
||||
// Halve oversized output buffer (IdentityMap pattern: gradual shrink after spike)
|
||||
if (_buffer.Length > _initialBufferSize * BufferHalvingThreshold)
|
||||
{
|
||||
var nextSize = Math.Max(_buffer.Length / 2, _initialBufferSize);
|
||||
ArrayPool<byte>.Shared.Return(_buffer);
|
||||
_buffer = ArrayPool<byte>.Shared.Rent(nextSize);
|
||||
}
|
||||
|
||||
// Clear wrapper tracking - returns IdentityMap arrays to pool
|
||||
base.Clear();
|
||||
}
|
||||
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_buffer != null)
|
||||
{
|
||||
ArrayPool<byte>.Shared.Return(_buffer);
|
||||
_buffer = null!;
|
||||
}
|
||||
|
||||
if (_propertyIndexBuffer != null)
|
||||
{
|
||||
ArrayPool<int>.Shared.Return(_propertyIndexBuffer);
|
||||
|
|
@ -248,6 +192,9 @@ public static partial class AcBinarySerializer
|
|||
_propertyStateBuffer = null;
|
||||
}
|
||||
|
||||
// Dispose the output if it implements IDisposable (e.g. ArrayBinaryOutput returns buffer to pool)
|
||||
if (Output is IDisposable disposableOutput)
|
||||
disposableOutput.Dispose();
|
||||
}
|
||||
|
||||
#region String Interning
|
||||
|
|
@ -262,7 +209,7 @@ public static partial class AcBinarySerializer
|
|||
if (_stringInternMap == null)
|
||||
{
|
||||
found = false;
|
||||
return ref System.Runtime.CompilerServices.Unsafe.NullRef<InternEntry>();
|
||||
return ref Unsafe.NullRef<InternEntry>();
|
||||
}
|
||||
|
||||
if (_stringInternMap.TryAdd(value, out var slotIndex))
|
||||
|
|
@ -312,7 +259,6 @@ public static partial class AcBinarySerializer
|
|||
/// </summary>
|
||||
public int GetCacheCount() => _nextCacheIndex;
|
||||
|
||||
|
||||
#endregion
|
||||
|
||||
#region UseMetadata Type Tracking
|
||||
|
|
@ -338,17 +284,17 @@ public static partial class AcBinarySerializer
|
|||
/// Ismételt: [propNameHash (4b)]
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void WriteInlineMetadata(BinarySerializeTypeMetadata metadata, bool isFirstOccurrence)
|
||||
public void WriteInlineMetadata(BinarySerializeTypeMetadata metadata, TOutput output, bool isFirstOccurrence)
|
||||
{
|
||||
WriteRaw(metadata.PropNameHash);
|
||||
output.WriteRaw(metadata.PropNameHash);
|
||||
|
||||
if (isFirstOccurrence)
|
||||
{
|
||||
var hashes = metadata.MetadataPropertyHashes;
|
||||
WriteVarUInt((uint)hashes.Length);
|
||||
output.WriteVarUInt((uint)hashes.Length);
|
||||
for (var i = 0; i < hashes.Length; i++)
|
||||
{
|
||||
WriteRaw(hashes[i]);
|
||||
output.WriteRaw(hashes[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -382,42 +328,6 @@ public static partial class AcBinarySerializer
|
|||
|
||||
#endregion
|
||||
|
||||
#region Output
|
||||
|
||||
/// <summary>
|
||||
/// Returns the serialized data as a ReadOnlySpan without allocation.
|
||||
/// Use this for compression or other processing before final ToArray().
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public ReadOnlySpan<byte> AsSpan() => _buffer.AsSpan(0, _position);
|
||||
|
||||
public byte[] ToArray()
|
||||
{
|
||||
var result = GC.AllocateUninitializedArray<byte>(_position);
|
||||
_buffer.AsSpan(0, _position).CopyTo(result);
|
||||
return result;
|
||||
}
|
||||
|
||||
public void WriteTo(IBufferWriter<byte> writer)
|
||||
{
|
||||
var span = writer.GetSpan(_position);
|
||||
_buffer.AsSpan(0, _position).CopyTo(span);
|
||||
writer.Advance(_position);
|
||||
}
|
||||
|
||||
public BinarySerializationResult DetachResult()
|
||||
{
|
||||
var resultBuffer = _buffer;
|
||||
var resultLength = _position;
|
||||
|
||||
_buffer = ArrayPool<byte>.Shared.Rent(_initialBufferSize);
|
||||
_position = 0;
|
||||
|
||||
return new BinarySerializationResult(resultBuffer, resultLength, pooled: true);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Property Filtering
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
|
|
@ -441,440 +351,8 @@ public static partial class AcBinarySerializer
|
|||
|
||||
#endregion
|
||||
|
||||
#region Buffer Helpers
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private void EnsureCapacity(int additionalBytes)
|
||||
{
|
||||
var required = _position + additionalBytes;
|
||||
if (required <= _buffer.Length)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
GrowBuffer(required);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
private void GrowBuffer(int required)
|
||||
{
|
||||
var newSize = Math.Max(_buffer.Length * 2, required);
|
||||
var newBuffer = ArrayPool<byte>.Shared.Rent(newSize);
|
||||
_buffer.AsSpan(0, _position).CopyTo(newBuffer);
|
||||
ArrayPool<byte>.Shared.Return(_buffer);
|
||||
_buffer = newBuffer;
|
||||
|
||||
#if DEBUG
|
||||
GrowBufferCount++;
|
||||
GrowBufferTotalBytes += newSize;
|
||||
#endif
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void WriteByte(byte value)
|
||||
{
|
||||
if (_position >= _buffer.Length)
|
||||
{
|
||||
GrowBuffer(_position + 1);
|
||||
}
|
||||
_buffer[_position++] = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Write type code byte followed by a raw value. Batches EnsureCapacity call.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void WriteTypeCodeAndRaw<T>(byte typeCode, T value) where T : unmanaged
|
||||
{
|
||||
var size = 1 + Unsafe.SizeOf<T>();
|
||||
EnsureCapacity(size);
|
||||
_buffer[_position++] = typeCode;
|
||||
Unsafe.WriteUnaligned(ref _buffer[_position], value);
|
||||
_position += Unsafe.SizeOf<T>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Write two bytes efficiently.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void WriteTwoBytes(byte b1, byte b2)
|
||||
{
|
||||
EnsureCapacity(2);
|
||||
_buffer[_position++] = b1;
|
||||
_buffer[_position++] = b2;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void WriteBytes(ReadOnlySpan<byte> data)
|
||||
{
|
||||
EnsureCapacity(data.Length);
|
||||
data.CopyTo(_buffer.AsSpan(_position));
|
||||
_position += data.Length;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void WriteRaw<T>(T value) where T : unmanaged
|
||||
{
|
||||
var size = Unsafe.SizeOf<T>();
|
||||
EnsureCapacity(size);
|
||||
Unsafe.WriteUnaligned(ref _buffer[_position], value);
|
||||
_position += size;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Specialized Writers
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void WriteDecimalBits(decimal value)
|
||||
{
|
||||
EnsureCapacity(16);
|
||||
Span<int> bits = stackalloc int[4];
|
||||
decimal.TryGetBits(value, bits, out _);
|
||||
MemoryMarshal.AsBytes(bits).CopyTo(_buffer.AsSpan(_position, 16));
|
||||
_position += 16;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void WriteDateTimeBits(DateTime value)
|
||||
{
|
||||
EnsureCapacity(9);
|
||||
Unsafe.WriteUnaligned(ref _buffer[_position], value.Ticks);
|
||||
_buffer[_position + 8] = (byte)value.Kind;
|
||||
_position += 9;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void WriteGuidBits(Guid value)
|
||||
{
|
||||
EnsureCapacity(16);
|
||||
value.TryWriteBytes(_buffer.AsSpan(_position, 16));
|
||||
_position += 16;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void WriteDateTimeOffsetBits(DateTimeOffset value)
|
||||
{
|
||||
EnsureCapacity(10);
|
||||
Unsafe.WriteUnaligned(ref _buffer[_position], value.UtcTicks);
|
||||
Unsafe.WriteUnaligned(ref _buffer[_position + 8], (short)value.Offset.TotalMinutes);
|
||||
_position += 10;
|
||||
}
|
||||
|
||||
|
||||
public void WriteVarInt(int value)
|
||||
{
|
||||
var encoded = (uint)((value << 1) ^ (value >> 31));
|
||||
// Fast path for small positive values (0-63 when ZigZag encoded)
|
||||
if (encoded < 0x80)
|
||||
{
|
||||
EnsureCapacity(1);
|
||||
_buffer[_position++] = (byte)encoded;
|
||||
return;
|
||||
}
|
||||
EnsureCapacity(5);
|
||||
WriteVarUIntInternal(encoded);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void WriteVarUInt(uint value)
|
||||
{
|
||||
// Fast path for small values (0-127)
|
||||
if (value < 0x80)
|
||||
{
|
||||
EnsureCapacity(1);
|
||||
_buffer[_position++] = (byte)value;
|
||||
return;
|
||||
}
|
||||
EnsureCapacity(5);
|
||||
WriteVarUIntInternal(value);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private void WriteVarUIntInternal(uint value)
|
||||
{
|
||||
while (value >= 0x80)
|
||||
{
|
||||
_buffer[_position++] = (byte)(value | 0x80);
|
||||
value >>= 7;
|
||||
}
|
||||
|
||||
_buffer[_position++] = (byte)value;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void WriteVarLong(long value)
|
||||
{
|
||||
var encoded = (ulong)((value << 1) ^ (value >> 63));
|
||||
// Fast path for small values
|
||||
if (encoded < 0x80)
|
||||
{
|
||||
EnsureCapacity(1);
|
||||
_buffer[_position++] = (byte)encoded;
|
||||
return;
|
||||
}
|
||||
EnsureCapacity(10);
|
||||
WriteVarULongInternal(encoded);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void WriteVarULong(ulong value)
|
||||
{
|
||||
// Fast path for small values (0-127)
|
||||
if (value < 0x80)
|
||||
{
|
||||
EnsureCapacity(1);
|
||||
_buffer[_position++] = (byte)value;
|
||||
return;
|
||||
}
|
||||
EnsureCapacity(10);
|
||||
WriteVarULongInternal(value);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private void WriteVarULongInternal(ulong value)
|
||||
{
|
||||
while (value >= 0x80)
|
||||
{
|
||||
_buffer[_position++] = (byte)(value | 0x80);
|
||||
value >>= 7;
|
||||
}
|
||||
|
||||
_buffer[_position++] = (byte)value;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void WriteStringUtf8(string value)
|
||||
{
|
||||
// Fast path for ASCII-only strings using SIMD-optimized check
|
||||
if (Ascii.IsValid(value))
|
||||
{
|
||||
WriteVarUInt((uint)value.Length);
|
||||
EnsureCapacity(value.Length);
|
||||
// Use System.Text.Ascii for SIMD-optimized ASCII to bytes conversion
|
||||
Ascii.FromUtf16(value.AsSpan(), _buffer.AsSpan(_position, value.Length), out _);
|
||||
_position += value.Length;
|
||||
return;
|
||||
}
|
||||
|
||||
// Standard path for multi-byte UTF8
|
||||
var byteCount = Utf8NoBom.GetByteCount(value);
|
||||
WriteVarUInt((uint)byteCount);
|
||||
EnsureCapacity(byteCount);
|
||||
Utf8NoBom.GetBytes(value.AsSpan(), _buffer.AsSpan(_position, byteCount));
|
||||
_position += byteCount;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if string contains only ASCII characters (0-127).
|
||||
/// Optimized loop with early exit.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static bool IsAscii(string value)
|
||||
{
|
||||
var span = value.AsSpan();
|
||||
for (var i = 0; i < span.Length; i++)
|
||||
{
|
||||
if (span[i] > 127)
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Writes ASCII string directly to byte buffer (char to byte, no encoding needed).
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static void WriteAsciiDirect(ReadOnlySpan<char> source, Span<byte> destination)
|
||||
{
|
||||
for (var i = 0; i < source.Length; i++)
|
||||
{
|
||||
destination[i] = (byte)source[i];
|
||||
}
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void WritePreencodedPropertyName(ReadOnlySpan<byte> utf8Name)
|
||||
{
|
||||
WriteByte(BinaryTypeCode.String);
|
||||
WriteVarUInt((uint)utf8Name.Length);
|
||||
WriteBytes(utf8Name);
|
||||
}
|
||||
|
||||
public void WriteInt32ArrayOptimized(int[] array)
|
||||
{
|
||||
for (var i = 0; i < array.Length; i++)
|
||||
{
|
||||
var value = array[i];
|
||||
if (BinaryTypeCode.TryEncodeTinyInt(value, out var tiny))
|
||||
{
|
||||
WriteByte(tiny);
|
||||
}
|
||||
else
|
||||
{
|
||||
WriteByte(BinaryTypeCode.Int32);
|
||||
WriteVarInt(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void WriteLongArrayOptimized(long[] array)
|
||||
{
|
||||
for (var i = 0; i < array.Length; i++)
|
||||
{
|
||||
var value = array[i];
|
||||
if (value >= int.MinValue && value <= int.MaxValue)
|
||||
{
|
||||
var intValue = (int)value;
|
||||
if (BinaryTypeCode.TryEncodeTinyInt(intValue, out var tiny))
|
||||
{
|
||||
WriteByte(tiny);
|
||||
}
|
||||
else
|
||||
{
|
||||
WriteByte(BinaryTypeCode.Int32);
|
||||
WriteVarInt(intValue);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
WriteByte(BinaryTypeCode.Int64);
|
||||
WriteVarLong(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void WriteDoubleArrayBulk(double[] array)
|
||||
{
|
||||
EnsureCapacity(array.Length * 9);
|
||||
for (var i = 0; i < array.Length; i++)
|
||||
{
|
||||
_buffer[_position++] = BinaryTypeCode.Float64;
|
||||
Unsafe.WriteUnaligned(ref _buffer[_position], array[i]);
|
||||
_position += 8;
|
||||
}
|
||||
}
|
||||
|
||||
public void WriteFloatArrayBulk(float[] array)
|
||||
{
|
||||
EnsureCapacity(array.Length * 5);
|
||||
for (var i = 0; i < array.Length; i++)
|
||||
{
|
||||
_buffer[_position++] = BinaryTypeCode.Float32;
|
||||
Unsafe.WriteUnaligned(ref _buffer[_position], array[i]);
|
||||
_position += 4;
|
||||
}
|
||||
}
|
||||
|
||||
public void WriteGuidArrayBulk(Guid[] array)
|
||||
{
|
||||
EnsureCapacity(array.Length * 17);
|
||||
for (var i = 0; i < array.Length; i++)
|
||||
{
|
||||
_buffer[_position++] = BinaryTypeCode.Guid;
|
||||
array[i].TryWriteBytes(_buffer.AsSpan(_position, 16));
|
||||
_position += 16;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region SIMD Bulk Copy
|
||||
|
||||
/// <summary>
|
||||
/// Copy bytes using SIMD when available, otherwise fall back to standard copy.
|
||||
/// Optimized for Blazor WASM where Vector operations are supported.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void WriteBytesSimd(ReadOnlySpan<byte> source)
|
||||
{
|
||||
EnsureCapacity(source.Length);
|
||||
var destination = _buffer.AsSpan(_position, source.Length);
|
||||
|
||||
if (Vector.IsHardwareAccelerated && source.Length >= Vector<byte>.Count * 2)
|
||||
{
|
||||
CopyWithSimd(source, destination);
|
||||
}
|
||||
else
|
||||
{
|
||||
source.CopyTo(destination);
|
||||
}
|
||||
|
||||
_position += source.Length;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// SIMD-optimized memory copy for large buffers.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static void CopyWithSimd(ReadOnlySpan<byte> source, Span<byte> destination)
|
||||
{
|
||||
var vectorSize = Vector<byte>.Count;
|
||||
var i = 0;
|
||||
var length = source.Length;
|
||||
|
||||
// Process full vectors
|
||||
var vectorCount = length / vectorSize;
|
||||
for (var v = 0; v < vectorCount; v++)
|
||||
{
|
||||
var vec = new Vector<byte>(source.Slice(i, vectorSize));
|
||||
vec.CopyTo(destination.Slice(i, vectorSize));
|
||||
i += vectorSize;
|
||||
}
|
||||
|
||||
// Copy remaining bytes
|
||||
if (i < length)
|
||||
{
|
||||
source.Slice(i).CopyTo(destination.Slice(i));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Write double array using SIMD bulk copy (no per-element type codes).
|
||||
/// For use when caller handles type codes separately.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void WriteDoubleBulkRaw(ReadOnlySpan<double> values)
|
||||
{
|
||||
var byteSpan = MemoryMarshal.AsBytes(values);
|
||||
WriteBytesSimd(byteSpan);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Write float array using SIMD bulk copy.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void WriteFloatBulkRaw(ReadOnlySpan<float> values)
|
||||
{
|
||||
var byteSpan = MemoryMarshal.AsBytes(values);
|
||||
WriteBytesSimd(byteSpan);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Write Guid array using SIMD bulk copy.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void WriteGuidBulkRaw(ReadOnlySpan<Guid> values)
|
||||
{
|
||||
// Guid is 16 bytes, perfect for SIMD
|
||||
var byteLength = values.Length * 16;
|
||||
EnsureCapacity(byteLength);
|
||||
|
||||
for (var i = 0; i < values.Length; i++)
|
||||
{
|
||||
values[i].TryWriteBytes(_buffer.AsSpan(_position, 16));
|
||||
_position += 16;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Header
|
||||
|
||||
// Marker-based interning: no footer needed
|
||||
// Header: [version][flags][cacheCount (VarUInt, if caching enabled)]
|
||||
// Body: data with markers (StringInternFirst, ObjectRefFirst, etc.)
|
||||
|
||||
/// <summary>
|
||||
/// Writes the binary header directly. Call AFTER ScanForDuplicates (cacheCount is known).
|
||||
/// No placeholder, no shift — single forward write.
|
||||
|
|
@ -892,48 +370,17 @@ public static partial class AcBinarySerializer
|
|||
if (HasCaching)
|
||||
flags |= BinaryTypeCode.HeaderFlag_HasCacheCount;
|
||||
|
||||
WriteByte(AcBinarySerializerOptions.FormatVersion);
|
||||
WriteByte(flags);
|
||||
Output.WriteByte(AcBinarySerializerOptions.FormatVersion);
|
||||
Output.WriteByte(flags);
|
||||
|
||||
if (HasCaching)
|
||||
{
|
||||
WriteVarUInt((uint)GetCacheCount());
|
||||
Output.WriteVarUInt((uint)GetCacheCount());
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Reference Handling
|
||||
|
||||
//[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
//public bool TrackForScanning(object obj) => _refTracker.TrackForScanning(obj);
|
||||
|
||||
/// <summary>
|
||||
/// IId-aware tracking for the scan phase.
|
||||
/// First checks IId match (different instance, same Id), then falls back to ReferenceEquals.
|
||||
/// </summary>
|
||||
//[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
//public bool TrackForScanningWithIId(object obj, BinarySerializeTypeMetadata metadata, out int existingRefId)
|
||||
//{
|
||||
// if (!ReferenceHandling)
|
||||
// {
|
||||
// existingRefId = 0;
|
||||
// return true; // No tracking needed
|
||||
// }
|
||||
// return _refTracker.TrackForScanningWithIId(obj, metadata, out existingRefId);
|
||||
//}
|
||||
|
||||
//[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
//public bool ShouldWriteRef(object obj, out int refId) => _refTracker.ShouldWriteId(obj, out refId);
|
||||
|
||||
//[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
//public void MarkAsWritten(object obj, int refId) => _refTracker.MarkAsWritten(obj, refId);
|
||||
|
||||
//[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
//public bool TryGetExistingRef(object obj, out int refId) => _refTracker.TryGetExistingRef(obj, out refId);
|
||||
|
||||
#endregion
|
||||
|
||||
#region Helpers
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
|
|
@ -968,79 +415,5 @@ public static partial class AcBinarySerializer
|
|||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region FixStr Methods
|
||||
|
||||
/// <summary>
|
||||
/// Write short ASCII string using FixStr encoding (type+length in single byte).
|
||||
/// Only call when string is ASCII and length <= 31.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void WriteFixStr(string value)
|
||||
{
|
||||
var length = value.Length;
|
||||
EnsureCapacity(1 + length);
|
||||
_buffer[_position++] = BinaryTypeCode.EncodeFixStr(length);
|
||||
Ascii.FromUtf16(value.AsSpan(), _buffer.AsSpan(_position, length), out _);
|
||||
_position += length;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Optimized FixStr write: tries SIMD ASCII conversion, falls back to UTF8.
|
||||
/// Single-pass: uses Ascii.FromUtf16 which does validation + copy.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void WriteFixStrDirect(string value)
|
||||
{
|
||||
var length = value.Length;
|
||||
EnsureCapacity(1 + length);
|
||||
|
||||
// Ascii.FromUtf16: SIMD-optimized ASCII conversion
|
||||
// Returns actual bytes written - if less than input length, there was a non-ASCII char
|
||||
var destSpan = _buffer.AsSpan(_position + 1, length);
|
||||
var status = Ascii.FromUtf16(value.AsSpan(), destSpan, out var bytesWritten);
|
||||
|
||||
if (status == System.Buffers.OperationStatus.Done && bytesWritten == length)
|
||||
{
|
||||
// Success - write FixStr header
|
||||
_buffer[_position] = BinaryTypeCode.EncodeFixStr(length);
|
||||
_position += 1 + length;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Non-ASCII or partial - use standard string encoding
|
||||
_buffer[_position++] = BinaryTypeCode.String;
|
||||
WriteStringUtf8Internal(value);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Internal string write (after String type code already written).
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private void WriteStringUtf8Internal(string value)
|
||||
{
|
||||
var byteCount = Utf8NoBom.GetByteCount(value);
|
||||
WriteVarUInt((uint)byteCount);
|
||||
EnsureCapacity(byteCount);
|
||||
Utf8NoBom.GetBytes(value.AsSpan(), _buffer.AsSpan(_position, byteCount));
|
||||
_position += byteCount;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Write short UTF8 bytes using FixStr encoding.
|
||||
/// Only call when byteLength <= 31.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void WriteFixStrBytes(ReadOnlySpan<byte> utf8Bytes)
|
||||
{
|
||||
var length = utf8Bytes.Length;
|
||||
EnsureCapacity(1 + length);
|
||||
_buffer[_position++] = BinaryTypeCode.EncodeFixStr(length);
|
||||
utf8Bytes.CopyTo(_buffer.AsSpan(_position, length));
|
||||
_position += length;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -144,7 +144,7 @@ public static partial class AcBinarySerializer
|
|||
NeedsReferenceTracking = IsIId || HasComplexProperties || !IsPrimitiveType;
|
||||
|
||||
// Fast check: only look for generated serializer if type has [AcBinarySerializable] attribute
|
||||
if (type.IsDefined(typeof(AcBinarySerializableAttribute), inherit: false))
|
||||
if (false && type.IsDefined(typeof(AcBinarySerializableAttribute), inherit: false))
|
||||
{
|
||||
GeneratedSerializerType = FindGeneratedSerializerType(type);
|
||||
HasGeneratedSerializer = GeneratedSerializerType != null;
|
||||
|
|
|
|||
|
|
@ -15,7 +15,8 @@ public static partial class AcBinarySerializer
|
|||
/// - Strings/objects skipped here are never written anyway (parent is ObjectRef)
|
||||
/// CacheIndex is assigned immediately on 2nd occurrence (no post-processing needed).
|
||||
/// </summary>
|
||||
private static void ScanForDuplicates(object value, Type type, BinarySerializationContext context)
|
||||
private static void ScanForDuplicates<TOutput>(object value, Type type, BinarySerializationContext<TOutput> context)
|
||||
where TOutput : BinaryOutputBase
|
||||
{
|
||||
if (!context.HasCaching)
|
||||
return;
|
||||
|
|
@ -24,7 +25,8 @@ public static partial class AcBinarySerializer
|
|||
ScanValue(value, wrapper, context, 0);
|
||||
}
|
||||
|
||||
private static void ScanValue(object? value, TypeMetadataWrapper<BinarySerializeTypeMetadata> wrapper, BinarySerializationContext context, int depth)
|
||||
private static void ScanValue<TOutput>(object? value, TypeMetadataWrapper<BinarySerializeTypeMetadata> wrapper, BinarySerializationContext<TOutput> context, int depth)
|
||||
where TOutput : BinaryOutputBase
|
||||
{
|
||||
if (value == null || depth > context.MaxDepth)
|
||||
return;
|
||||
|
|
@ -128,7 +130,8 @@ public static partial class AcBinarySerializer
|
|||
/// Scans a collection item. Handles string fast path and gets wrapper for the runtime type.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static void ScanItem(object item, BinarySerializationContext context, int depth)
|
||||
private static void ScanItem<TOutput>(object item, BinarySerializationContext<TOutput> context, int depth)
|
||||
where TOutput : BinaryOutputBase
|
||||
{
|
||||
// String fast path — avoid GetWrapper entirely
|
||||
if (item is string str)
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -82,7 +82,7 @@ public sealed class AcBinarySerializerOptions : AcSerializerOptions
|
|||
/// allowing the deserializer to match properties by name between different types.
|
||||
/// Default: false (no overhead)
|
||||
/// </summary>
|
||||
public bool UseMetadata { get; set; } = true;
|
||||
public bool UseMetadata { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// When true, checks for duplicate property name hashes during serialization (UseMetadata mode).
|
||||
|
|
|
|||
|
|
@ -398,6 +398,21 @@ public sealed class ArrayBinaryOutput : BinaryOutputBase, IDisposable
|
|||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void Reset() => _position = 0;
|
||||
|
||||
/// <summary>
|
||||
/// Detaches the internal buffer as a BinarySerializationResult and allocates a fresh buffer.
|
||||
/// The caller owns the returned result and must dispose it to return the buffer to the pool.
|
||||
/// </summary>
|
||||
public AcBinarySerializer.BinarySerializationResult DetachResult()
|
||||
{
|
||||
var resultBuffer = _buffer;
|
||||
var resultLength = _position;
|
||||
|
||||
_buffer = ArrayPool<byte>.Shared.Rent(Math.Max(resultBuffer.Length / 2, MinBufferSize));
|
||||
_position = 0;
|
||||
|
||||
return new AcBinarySerializer.BinarySerializationResult(resultBuffer, resultLength, pooled: true);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region IDisposable
|
||||
|
|
|
|||
|
|
@ -47,82 +47,26 @@ public abstract class BinaryOutputBase : IBinaryOutput
|
|||
|
||||
#endregion
|
||||
|
||||
#region Virtual — WriteTypeCodeAndRaw
|
||||
#region Abstract — WriteTypeCodeAndRaw
|
||||
|
||||
/// <inheritdoc />
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public virtual void WriteTypeCodeAndRaw<T>(byte typeCode, T value) where T : unmanaged
|
||||
{
|
||||
EnsureCapacity(1 + Unsafe.SizeOf<T>());
|
||||
WriteByte(typeCode);
|
||||
WriteRaw(value);
|
||||
}
|
||||
public abstract void WriteTypeCodeAndRaw<T>(byte typeCode, T value) where T : unmanaged;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Virtual — VarInt Encoding
|
||||
#region Abstract — VarInt Encoding
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual void WriteVarInt(int value)
|
||||
{
|
||||
var encoded = (uint)((value << 1) ^ (value >> 31));
|
||||
WriteVarUInt(encoded);
|
||||
}
|
||||
public abstract void WriteVarInt(int value);
|
||||
|
||||
/// <inheritdoc />
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public virtual void WriteVarUInt(uint value)
|
||||
{
|
||||
if (value < 0x80)
|
||||
{
|
||||
WriteByte((byte)value);
|
||||
return;
|
||||
}
|
||||
|
||||
WriteVarUIntMultiByte(value);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
private void WriteVarUIntMultiByte(uint value)
|
||||
{
|
||||
while (value >= 0x80)
|
||||
{
|
||||
WriteByte((byte)(value | 0x80));
|
||||
value >>= 7;
|
||||
}
|
||||
WriteByte((byte)value);
|
||||
}
|
||||
public abstract void WriteVarUInt(uint value);
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual void WriteVarLong(long value)
|
||||
{
|
||||
var encoded = (ulong)((value << 1) ^ (value >> 63));
|
||||
WriteVarULong(encoded);
|
||||
}
|
||||
public abstract void WriteVarLong(long value);
|
||||
|
||||
/// <inheritdoc />
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public virtual void WriteVarULong(ulong value)
|
||||
{
|
||||
if (value < 0x80)
|
||||
{
|
||||
WriteByte((byte)value);
|
||||
return;
|
||||
}
|
||||
|
||||
WriteVarULongMultiByte(value);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
private void WriteVarULongMultiByte(ulong value)
|
||||
{
|
||||
while (value >= 0x80)
|
||||
{
|
||||
WriteByte((byte)(value | 0x80));
|
||||
value >>= 7;
|
||||
}
|
||||
WriteByte((byte)value);
|
||||
}
|
||||
public abstract void WriteVarULong(ulong value);
|
||||
|
||||
#endregion
|
||||
|
||||
|
|
@ -137,11 +81,7 @@ public abstract class BinaryOutputBase : IBinaryOutput
|
|||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual void WriteDateTimeBits(DateTime value)
|
||||
{
|
||||
WriteRaw(value.Ticks);
|
||||
WriteByte((byte)value.Kind);
|
||||
}
|
||||
public abstract void WriteDateTimeBits(DateTime value);
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual void WriteGuidBits(Guid value)
|
||||
|
|
@ -152,11 +92,7 @@ public abstract class BinaryOutputBase : IBinaryOutput
|
|||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual void WriteDateTimeOffsetBits(DateTimeOffset value)
|
||||
{
|
||||
WriteRaw(value.UtcTicks);
|
||||
WriteRaw((short)value.Offset.TotalMinutes);
|
||||
}
|
||||
public abstract void WriteDateTimeOffsetBits(DateTimeOffset value);
|
||||
|
||||
#endregion
|
||||
|
||||
|
|
@ -247,39 +183,16 @@ public abstract class BinaryOutputBase : IBinaryOutput
|
|||
|
||||
#endregion
|
||||
|
||||
#region Virtual — Bulk Array Writes (overridable for optimization)
|
||||
#region Bulk Array Writes
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual void WriteDoubleArrayBulk(double[] array)
|
||||
{
|
||||
for (var i = 0; i < array.Length; i++)
|
||||
{
|
||||
WriteByte(BinaryTypeCode.Float64);
|
||||
WriteRaw(array[i]);
|
||||
}
|
||||
}
|
||||
public abstract void WriteDoubleArrayBulk(double[] array);
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual void WriteFloatArrayBulk(float[] array)
|
||||
{
|
||||
for (var i = 0; i < array.Length; i++)
|
||||
{
|
||||
WriteByte(BinaryTypeCode.Float32);
|
||||
WriteRaw(array[i]);
|
||||
}
|
||||
}
|
||||
public abstract void WriteFloatArrayBulk(float[] array);
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual void WriteGuidArrayBulk(Guid[] array)
|
||||
{
|
||||
Span<byte> buf = stackalloc byte[16];
|
||||
for (var i = 0; i < array.Length; i++)
|
||||
{
|
||||
WriteByte(BinaryTypeCode.Guid);
|
||||
array[i].TryWriteBytes(buf);
|
||||
WriteBytes(buf);
|
||||
}
|
||||
}
|
||||
public abstract void WriteGuidArrayBulk(Guid[] array);
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual void WriteInt32ArrayOptimized(int[] array)
|
||||
|
|
|
|||
Loading…
Reference in New Issue