perf(saves): 2.2x faster BufferWriter write path, single-pass short strings
Tier-1 disasm of a generated-style Serialize showed PGO's guarded devirtualization already inlines every IGenericWriter.Write body (interface vs concrete-typed callsites measured identical), so no API or generator changes are needed. What remained as a real call per primitive write was the Index property setter - too large to inline due to its range-check throw path, plus per-write high-water tracking, an AsSpan bounds check, and a BinaryPrimitives span check. The write path now reserves capacity once (the existing grow-on-Flush check), then does an unaligned store through a ref with a raw index increment - the capacity check proves the store in-bounds, and the index only moves forward between Seeks. The _bytesWritten high-water mark (used only by SeekOrigin.End) folds at Seek/Resize instead of per write, which is equivalent because writes are monotonic between seeks. The Index property keeps its validating semantics for Seek and subclasses. BufferWriter also gains class-level implementations of the hottest IGenericWriter default interface methods (WriteEncodedInt, DateTime, TimeSpan, Point2D/3D, Rectangle2D/3D, Map, Race): a DIM dispatches again on `this` for every nested Write even at a devirtualized callsite, and the class overloads keep the whole write inlined. Strings (arbitrary UTF-16) previously walked every string twice (GetByteCount then GetBytes) because the variable-width length prefix precedes the bytes. Strings of 85 chars or fewer (any content, incl. surrogate pairs - 85 * 3 = 255 bytes max) now encode once into a 256-byte stack scratch, then write the prefix and copy. Byte output is identical. Measured: 34.4 -> 15.7 ns/entity on a generated-style write mix (~20 writes, 58 bytes), 22.5 -> 11.8 ns per short-string write, and the end-to-end freeze benchmark (10M entities / 1.7GB / 24 cores, dense profile) drops from ~99ms to ~74-82ms. Combined with the earlier pipeline commits: ~740ms -> ~78ms. New tests pin byte-level output and position semantics: primitive little-endian layouts, Seek(End) high-water behavior, growth preservation, span writes across growth, encoded-int formats, and string equivalence across the scratch/two-pass boundary with mixed-width UTF-16 content. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
9acd701aaa
commit
f7b835d7e9
2 changed files with 412 additions and 46 deletions
254
Projects/Server.Tests/Tests/Serialization/BufferWriterTests.cs
Normal file
254
Projects/Server.Tests/Tests/Serialization/BufferWriterTests.cs
Normal file
|
|
@ -0,0 +1,254 @@
|
|||
using System;
|
||||
using System.Buffers.Binary;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using Xunit;
|
||||
|
||||
namespace Server.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Pins BufferWriter's byte-level output and position semantics so the write path can be
|
||||
/// optimized without behavioral drift.
|
||||
/// </summary>
|
||||
public class BufferWriterTests
|
||||
{
|
||||
[Fact]
|
||||
public void PrimitivesAreLittleEndianAtExpectedOffsets()
|
||||
{
|
||||
var writer = new BufferWriter(new byte[256], true);
|
||||
|
||||
writer.Write((byte)0xAB);
|
||||
writer.Write((sbyte)-5);
|
||||
writer.Write(true);
|
||||
writer.Write(false);
|
||||
writer.Write((short)-12345);
|
||||
writer.Write((ushort)54321);
|
||||
writer.Write(-123456789);
|
||||
writer.Write(3123456789u);
|
||||
writer.Write(-1234567890123456789L);
|
||||
writer.Write(12345678901234567890UL);
|
||||
writer.Write(1234.5678d);
|
||||
writer.Write(56.75f);
|
||||
writer.Write((Serial)0x40000001u);
|
||||
|
||||
Assert.Equal(1 + 1 + 1 + 1 + 2 + 2 + 4 + 4 + 8 + 8 + 8 + 4 + 4, writer.Position);
|
||||
|
||||
var b = writer.Buffer;
|
||||
Assert.Equal(0xAB, b[0]);
|
||||
Assert.Equal(unchecked((byte)-5), b[1]);
|
||||
Assert.Equal(1, b[2]);
|
||||
Assert.Equal(0, b[3]);
|
||||
Assert.Equal(-12345, BinaryPrimitives.ReadInt16LittleEndian(b.AsSpan(4)));
|
||||
Assert.Equal(54321, BinaryPrimitives.ReadUInt16LittleEndian(b.AsSpan(6)));
|
||||
Assert.Equal(-123456789, BinaryPrimitives.ReadInt32LittleEndian(b.AsSpan(8)));
|
||||
Assert.Equal(3123456789u, BinaryPrimitives.ReadUInt32LittleEndian(b.AsSpan(12)));
|
||||
Assert.Equal(-1234567890123456789L, BinaryPrimitives.ReadInt64LittleEndian(b.AsSpan(16)));
|
||||
Assert.Equal(12345678901234567890UL, BinaryPrimitives.ReadUInt64LittleEndian(b.AsSpan(24)));
|
||||
Assert.Equal(1234.5678d, BinaryPrimitives.ReadDoubleLittleEndian(b.AsSpan(32)));
|
||||
Assert.Equal(56.75f, BinaryPrimitives.ReadSingleLittleEndian(b.AsSpan(40)));
|
||||
Assert.Equal(0x40000001u, BinaryPrimitives.ReadUInt32LittleEndian(b.AsSpan(44)));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SeekEndUsesHighWaterMarkNotCurrentPosition()
|
||||
{
|
||||
var writer = new BufferWriter(new byte[256], true);
|
||||
|
||||
writer.Write(1L);
|
||||
writer.Write(2L);
|
||||
writer.Write(3L); // high water = 24
|
||||
|
||||
writer.Seek(4, SeekOrigin.Begin);
|
||||
writer.Write(99); // position now 8, high water still 24
|
||||
|
||||
Assert.Equal(24, writer.Seek(0, SeekOrigin.End));
|
||||
Assert.Equal(24, writer.Position);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SeekCurrentAndBeginBehave()
|
||||
{
|
||||
var writer = new BufferWriter(new byte[64], true);
|
||||
|
||||
writer.Write(0xDEADBEEF);
|
||||
Assert.Equal(2, writer.Seek(2, SeekOrigin.Begin));
|
||||
Assert.Equal(3, writer.Seek(1, SeekOrigin.Current));
|
||||
|
||||
writer.Write((byte)0x77);
|
||||
Assert.Equal(0x77, writer.Buffer[3]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GrowthPreservesContentAndPosition()
|
||||
{
|
||||
var writer = new BufferWriter(new byte[16], true);
|
||||
|
||||
for (var i = 0; i < 100; i++)
|
||||
{
|
||||
writer.Write((long)i);
|
||||
}
|
||||
|
||||
Assert.Equal(800, writer.Position);
|
||||
Assert.True(writer.Buffer.Length >= 800);
|
||||
|
||||
for (var i = 0; i < 100; i++)
|
||||
{
|
||||
Assert.Equal(i, BinaryPrimitives.ReadInt64LittleEndian(writer.Buffer.AsSpan(i * 8)));
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SpanWriteCrossesGrowthBoundary()
|
||||
{
|
||||
var writer = new BufferWriter(new byte[8], true);
|
||||
|
||||
Span<byte> payload = stackalloc byte[64];
|
||||
for (var i = 0; i < payload.Length; i++)
|
||||
{
|
||||
payload[i] = (byte)(i + 1);
|
||||
}
|
||||
|
||||
writer.Write((ushort)7);
|
||||
writer.Write(payload);
|
||||
|
||||
Assert.Equal(66, writer.Position);
|
||||
Assert.Equal(payload.ToArray(), writer.Buffer.AsSpan(2, 64).ToArray());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(0, new byte[] { 0x00 })]
|
||||
[InlineData(127, new byte[] { 0x7F })]
|
||||
[InlineData(128, new byte[] { 0x80, 0x01 })]
|
||||
[InlineData(0x3FFF, new byte[] { 0xFF, 0x7F })]
|
||||
[InlineData(int.MaxValue, new byte[] { 0xFF, 0xFF, 0xFF, 0xFF, 0x07 })]
|
||||
[InlineData(-1, new byte[] { 0xFF, 0xFF, 0xFF, 0xFF, 0x0F })]
|
||||
public void EncodedIntMatchesFormat(int value, byte[] expected)
|
||||
{
|
||||
var writer = new BufferWriter(new byte[16], true);
|
||||
|
||||
((IGenericWriter)writer).WriteEncodedInt(value);
|
||||
|
||||
Assert.Equal(expected.Length, writer.Position);
|
||||
Assert.Equal(expected, writer.Buffer.AsSpan(0, expected.Length).ToArray());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PrefixedStringsWriteFlagLengthAndUtf8()
|
||||
{
|
||||
var writer = new BufferWriter(new byte[256], true);
|
||||
|
||||
writer.Write("héllo Ωorld");
|
||||
var utf8 = Encoding.UTF8.GetBytes("héllo Ωorld");
|
||||
|
||||
var b = writer.Buffer;
|
||||
Assert.Equal(1, b[0]); // not-null flag
|
||||
Assert.Equal(utf8.Length, b[1]); // encoded length (small string = 1 byte)
|
||||
Assert.Equal(utf8, b.AsSpan(2, utf8.Length).ToArray());
|
||||
Assert.Equal(2 + utf8.Length, writer.Position);
|
||||
|
||||
writer.Write((string)null);
|
||||
Assert.Equal(0, b[2 + utf8.Length]); // null flag
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(84)] // scratch path
|
||||
[InlineData(85)] // scratch path boundary
|
||||
[InlineData(86)] // two-pass path
|
||||
[InlineData(300)] // two-pass, length prefix > 1 byte
|
||||
public void StringPathsAgreeAcrossTheScratchBoundary(int chars)
|
||||
{
|
||||
// Mixed ASCII, 2-byte, 3-byte, and surrogate-pair (4-byte) content
|
||||
var builder = new StringBuilder(chars);
|
||||
for (var i = 0; builder.Length < chars; i++)
|
||||
{
|
||||
switch (i % 4)
|
||||
{
|
||||
case 0:
|
||||
builder.Append('a');
|
||||
break;
|
||||
case 1:
|
||||
builder.Append('é');
|
||||
break;
|
||||
case 2:
|
||||
builder.Append('Ω');
|
||||
break;
|
||||
default:
|
||||
if (builder.Length + 2 <= chars)
|
||||
{
|
||||
builder.Append("𝔘"); // surrogate pair
|
||||
}
|
||||
else
|
||||
{
|
||||
builder.Append('z');
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
var value = builder.ToString();
|
||||
Assert.Equal(chars, value.Length);
|
||||
|
||||
var writer = new BufferWriter(new byte[16], true); // forces growth through both paths
|
||||
writer.Write(value);
|
||||
|
||||
var utf8 = Encoding.UTF8.GetBytes(value);
|
||||
var b = writer.Buffer;
|
||||
Assert.Equal(1, b[0]);
|
||||
|
||||
// decode the 7-bit encoded length prefix
|
||||
var offset = 1;
|
||||
var length = 0;
|
||||
var shift = 0;
|
||||
byte current;
|
||||
do
|
||||
{
|
||||
current = b[offset++];
|
||||
length |= (current & 0x7F) << shift;
|
||||
shift += 7;
|
||||
} while ((current & 0x80) != 0);
|
||||
|
||||
Assert.Equal(utf8.Length, length);
|
||||
Assert.Equal(utf8, b.AsSpan(offset, length).ToArray());
|
||||
Assert.Equal(offset + length, writer.Position);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DateTimeWritesUtcTicksViaInterface()
|
||||
{
|
||||
var writer = new BufferWriter(new byte[64], true);
|
||||
IGenericWriter iface = writer;
|
||||
|
||||
var utc = new DateTime(2026, 7, 13, 1, 2, 3, DateTimeKind.Utc);
|
||||
var local = utc.ToLocalTime();
|
||||
|
||||
iface.Write(utc);
|
||||
iface.Write(local); // must convert to UTC
|
||||
|
||||
Assert.Equal(utc.Ticks, BinaryPrimitives.ReadInt64LittleEndian(writer.Buffer.AsSpan(0)));
|
||||
Assert.Equal(utc.Ticks, BinaryPrimitives.ReadInt64LittleEndian(writer.Buffer.AsSpan(8)));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Point3DWritesThreeInts()
|
||||
{
|
||||
var writer = new BufferWriter(new byte[64], true);
|
||||
IGenericWriter iface = writer;
|
||||
|
||||
iface.Write(new Point3D(100, -200, 30));
|
||||
|
||||
Assert.Equal(12, writer.Position);
|
||||
Assert.Equal(100, BinaryPrimitives.ReadInt32LittleEndian(writer.Buffer.AsSpan(0)));
|
||||
Assert.Equal(-200, BinaryPrimitives.ReadInt32LittleEndian(writer.Buffer.AsSpan(4)));
|
||||
Assert.Equal(30, BinaryPrimitives.ReadInt32LittleEndian(writer.Buffer.AsSpan(8)));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DecimalRoundTripsThroughReader()
|
||||
{
|
||||
var writer = new BufferWriter(new byte[64], true);
|
||||
writer.Write(1234567.89012m);
|
||||
|
||||
IGenericReader reader = new BufferReader(writer.Buffer);
|
||||
Assert.Equal(1234567.89012m, reader.ReadDecimal());
|
||||
}
|
||||
}
|
||||
|
|
@ -30,6 +30,10 @@ public class BufferWriter : IGenericWriter
|
|||
private readonly ConcurrentQueue<Type> _types;
|
||||
private readonly Encoding _encoding;
|
||||
private readonly bool _prefixStrings;
|
||||
|
||||
// High-water mark for SeekOrigin.End. Writes advance _index directly and this is folded
|
||||
// in Seek/Resize (the only places the index can move backward), so the hot write path
|
||||
// carries no per-write bookkeeping.
|
||||
private long _bytesWritten;
|
||||
private long _index;
|
||||
|
||||
|
|
@ -76,7 +80,7 @@ public class BufferWriter : IGenericWriter
|
|||
_types = types;
|
||||
}
|
||||
|
||||
public virtual long Position => Index;
|
||||
public virtual long Position => _index;
|
||||
|
||||
protected virtual int BufferSize => 256;
|
||||
|
||||
|
|
@ -89,6 +93,8 @@ public class BufferWriter : IGenericWriter
|
|||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void Resize(int size)
|
||||
{
|
||||
_bytesWritten = Math.Max(_bytesWritten, _index);
|
||||
|
||||
// We shouldn't ever resize to a 0 length buffer. That is dangerous
|
||||
if (size <= 0)
|
||||
{
|
||||
|
|
@ -107,13 +113,23 @@ public class BufferWriter : IGenericWriter
|
|||
|
||||
public virtual void Flush() => Resize(Math.Clamp(_buffer.Length * 2, BufferSize, _buffer.Length + 1024 * 1024 * 64));
|
||||
|
||||
/// <summary>
|
||||
/// Ensures capacity, returns a ref at the current position, and advances the index.
|
||||
/// The capacity check proves the caller's unaligned store is in-bounds, and the index
|
||||
/// only moves forward between Seek calls, so no per-write validation is needed. Growth
|
||||
/// (Flush -> Resize) always adds at least BufferSize, covering any primitive width.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private void FlushIfNeeded(int amount)
|
||||
private ref byte Reserve(int bytes)
|
||||
{
|
||||
if (Index + amount > _buffer.Length)
|
||||
if (_index + bytes > _buffer.Length)
|
||||
{
|
||||
Flush();
|
||||
}
|
||||
|
||||
ref var result = ref Unsafe.Add(ref MemoryMarshal.GetArrayDataReference(_buffer), (nint)_index);
|
||||
_index += bytes;
|
||||
return ref result;
|
||||
}
|
||||
|
||||
public virtual void Write(byte[] bytes) => Write(bytes.AsSpan());
|
||||
|
|
@ -130,7 +146,7 @@ public class BufferWriter : IGenericWriter
|
|||
}
|
||||
|
||||
bytes.CopyTo(_buffer.AsSpan((int)_index));
|
||||
Index += length;
|
||||
_index += length;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
|
|
@ -149,9 +165,11 @@ public class BufferWriter : IGenericWriter
|
|||
"Attempting to seek to an invalid position using SeekOrigin.Current"
|
||||
);
|
||||
|
||||
_bytesWritten = Math.Max(_bytesWritten, _index);
|
||||
|
||||
return Index = Math.Max(0, origin switch
|
||||
{
|
||||
SeekOrigin.Current => Index + offset,
|
||||
SeekOrigin.Current => _index + offset,
|
||||
SeekOrigin.End => _bytesWritten + offset,
|
||||
_ => offset // Begin
|
||||
});
|
||||
|
|
@ -181,95 +199,99 @@ public class BufferWriter : IGenericWriter
|
|||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void Write(long value)
|
||||
{
|
||||
FlushIfNeeded(8);
|
||||
if (!BitConverter.IsLittleEndian)
|
||||
{
|
||||
value = BinaryPrimitives.ReverseEndianness(value);
|
||||
}
|
||||
|
||||
BinaryPrimitives.WriteInt64LittleEndian(_buffer.AsSpan((int)_index), value);
|
||||
Index += 8;
|
||||
Unsafe.WriteUnaligned(ref Reserve(8), value);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void Write(ulong value)
|
||||
{
|
||||
FlushIfNeeded(8);
|
||||
if (!BitConverter.IsLittleEndian)
|
||||
{
|
||||
value = BinaryPrimitives.ReverseEndianness(value);
|
||||
}
|
||||
|
||||
BinaryPrimitives.WriteUInt64LittleEndian(_buffer.AsSpan((int)_index), value);
|
||||
Index += 8;
|
||||
Unsafe.WriteUnaligned(ref Reserve(8), value);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void Write(int value)
|
||||
{
|
||||
FlushIfNeeded(4);
|
||||
if (!BitConverter.IsLittleEndian)
|
||||
{
|
||||
value = BinaryPrimitives.ReverseEndianness(value);
|
||||
}
|
||||
|
||||
BinaryPrimitives.WriteInt32LittleEndian(_buffer.AsSpan((int)_index), value);
|
||||
Index += 4;
|
||||
Unsafe.WriteUnaligned(ref Reserve(4), value);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void Write(uint value)
|
||||
{
|
||||
FlushIfNeeded(4);
|
||||
if (!BitConverter.IsLittleEndian)
|
||||
{
|
||||
value = BinaryPrimitives.ReverseEndianness(value);
|
||||
}
|
||||
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(_buffer.AsSpan((int)_index), value);
|
||||
Index += 4;
|
||||
Unsafe.WriteUnaligned(ref Reserve(4), value);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void Write(short value)
|
||||
{
|
||||
FlushIfNeeded(2);
|
||||
if (!BitConverter.IsLittleEndian)
|
||||
{
|
||||
value = BinaryPrimitives.ReverseEndianness(value);
|
||||
}
|
||||
|
||||
BinaryPrimitives.WriteInt16LittleEndian(_buffer.AsSpan((int)_index), value);
|
||||
Index += 2;
|
||||
Unsafe.WriteUnaligned(ref Reserve(2), value);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void Write(ushort value)
|
||||
{
|
||||
FlushIfNeeded(2);
|
||||
if (!BitConverter.IsLittleEndian)
|
||||
{
|
||||
value = BinaryPrimitives.ReverseEndianness(value);
|
||||
}
|
||||
|
||||
BinaryPrimitives.WriteUInt16LittleEndian(_buffer.AsSpan((int)_index), value);
|
||||
Index += 2;
|
||||
Unsafe.WriteUnaligned(ref Reserve(2), value);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void Write(double value)
|
||||
{
|
||||
FlushIfNeeded(8);
|
||||
if (!BitConverter.IsLittleEndian)
|
||||
{
|
||||
value = BitConverter.Int64BitsToDouble(BinaryPrimitives.ReverseEndianness(BitConverter.DoubleToInt64Bits(value)));
|
||||
}
|
||||
|
||||
BinaryPrimitives.WriteDoubleLittleEndian(_buffer.AsSpan((int)_index), value);
|
||||
Index += 8;
|
||||
Unsafe.WriteUnaligned(ref Reserve(8), value);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void Write(float value)
|
||||
{
|
||||
FlushIfNeeded(4);
|
||||
if (!BitConverter.IsLittleEndian)
|
||||
{
|
||||
value = BitConverter.Int32BitsToSingle(BinaryPrimitives.ReverseEndianness(BitConverter.SingleToInt32Bits(value)));
|
||||
}
|
||||
|
||||
BinaryPrimitives.WriteSingleLittleEndian(_buffer.AsSpan((int)_index), value);
|
||||
Index += 4;
|
||||
Unsafe.WriteUnaligned(ref Reserve(4), value);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void Write(byte value)
|
||||
{
|
||||
FlushIfNeeded(1);
|
||||
_buffer[Index++] = value;
|
||||
}
|
||||
public void Write(byte value) => Reserve(1) = value;
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void Write(sbyte value)
|
||||
{
|
||||
FlushIfNeeded(1);
|
||||
_buffer[Index++] = (byte)value;
|
||||
}
|
||||
public void Write(sbyte value) => Reserve(1) = (byte)value;
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public unsafe void Write(bool value)
|
||||
{
|
||||
FlushIfNeeded(1);
|
||||
_buffer[Index++] = *(byte*)&value; // up to 30% faster to dereference the raw value on the stack
|
||||
}
|
||||
public void Write(bool value) => Reserve(1) = Unsafe.As<bool, byte>(ref value);
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void Write(Serial serial) => Write(serial.Value);
|
||||
|
|
@ -298,12 +320,102 @@ public class BufferWriter : IGenericWriter
|
|||
Write(MemoryMarshal.Cast<int, byte>(buffer));
|
||||
}
|
||||
|
||||
// Class-level implementations of the hottest IGenericWriter default interface methods.
|
||||
// The JIT devirtualizes and inlines interface calls to the concrete type, but a default
|
||||
// interface method dispatches again internally on `this` for every nested Write — these
|
||||
// keep the whole write inlined instead.
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void WriteEncodedInt(int value)
|
||||
{
|
||||
var v = (uint)value;
|
||||
|
||||
while (v >= 0x80)
|
||||
{
|
||||
Write((byte)(v | 0x80));
|
||||
v >>= 7;
|
||||
}
|
||||
|
||||
Write((byte)v);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void Write(DateTime value)
|
||||
{
|
||||
// If DateTimeKind is Unspecified, we can't assume it needs to be converted.
|
||||
if (value.Kind == DateTimeKind.Local)
|
||||
{
|
||||
value = value.ToUniversalTime();
|
||||
}
|
||||
|
||||
Write(value.Ticks);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void Write(TimeSpan value) => Write(value.Ticks);
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void Write(Point3D value)
|
||||
{
|
||||
Write(value.m_X);
|
||||
Write(value.m_Y);
|
||||
Write(value.m_Z);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void Write(Point2D value)
|
||||
{
|
||||
Write(value.m_X);
|
||||
Write(value.m_Y);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void Write(Rectangle2D value)
|
||||
{
|
||||
Write(value.Start);
|
||||
Write(value.End);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void Write(Rectangle3D value)
|
||||
{
|
||||
Write(value.Start);
|
||||
Write(value.End);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void Write(Map value) => Write((byte)(value?.MapIndex ?? 0xFF));
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void Write(Race value) => Write((byte)(value?.RaceIndex ?? 0xFF));
|
||||
|
||||
internal void InternalWriteString(string value)
|
||||
{
|
||||
// Single pass for typical (short) strings: encode into a stack scratch, then write the
|
||||
// length prefix and copy. The two-pass path below walks the string twice
|
||||
// (GetByteCount + GetBytes) because the variable-width prefix must precede the bytes.
|
||||
// UTF8 needs at most 3 bytes per non-surrogate char (surrogate pairs encode 2 chars
|
||||
// into 4 bytes), so 85 chars always fit 255 bytes.
|
||||
if (value.Length <= 85)
|
||||
{
|
||||
Span<byte> scratch = stackalloc byte[256];
|
||||
var written = _encoding.GetBytes(value, scratch);
|
||||
|
||||
WriteEncodedInt(written);
|
||||
|
||||
while (_buffer.Length - _index < written)
|
||||
{
|
||||
Flush();
|
||||
}
|
||||
|
||||
scratch[..written].CopyTo(_buffer.AsSpan((int)_index));
|
||||
_index += written;
|
||||
return;
|
||||
}
|
||||
|
||||
var length = _encoding.GetByteCount(value);
|
||||
|
||||
((IGenericWriter)this).WriteEncodedInt(length);
|
||||
WriteEncodedInt(length);
|
||||
|
||||
while (_buffer.Length - _index < length)
|
||||
{
|
||||
|
|
@ -311,6 +423,6 @@ public class BufferWriter : IGenericWriter
|
|||
}
|
||||
|
||||
// We don't use spans here since that incurs extra allocations for safety.
|
||||
Index += _encoding.GetBytes(value, 0, value.Length, _buffer, (int)_index);
|
||||
_index += _encoding.GetBytes(value, 0, value.Length, _buffer, (int)_index);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue