403 lines
9.8 KiB
C#
403 lines
9.8 KiB
C#
using System;
|
|
using System.Buffers;
|
|
using System.Diagnostics;
|
|
using System.IO;
|
|
using System.Text;
|
|
using System.Text.Unicode;
|
|
using BenchmarkDotNet.Attributes;
|
|
using Lidgren.Network;
|
|
using NetSerializer;
|
|
|
|
namespace Content.Benchmarks
|
|
{
|
|
// Code for the *Slow and *Unsafe implementations taken from NetSerializer, licensed under the MIT license.
|
|
|
|
[MemoryDiagnoser]
|
|
public sealed class NetSerializerStringBenchmark
|
|
{
|
|
private const int StringByteBufferLength = 256;
|
|
private const int StringCharBufferLength = 128;
|
|
|
|
private string _toSerialize;
|
|
|
|
[Params(8, 64, 256, 1024)]
|
|
public int StringLength { get; set; }
|
|
|
|
private readonly MemoryStream _outputStream = new(2048);
|
|
private readonly MemoryStream _inputStream = new(2048);
|
|
|
|
[GlobalSetup]
|
|
public void Setup()
|
|
{
|
|
Span<byte> buf = stackalloc byte[StringLength / 2];
|
|
new Random().NextBytes(buf);
|
|
_toSerialize = NetUtility.ToHexString(buf);
|
|
Primitives.WritePrimitive(_inputStream, _toSerialize);
|
|
}
|
|
|
|
[Benchmark]
|
|
public void BenchWriteCore()
|
|
{
|
|
_outputStream.Position = 0;
|
|
WritePrimitiveCore(_outputStream, _toSerialize);
|
|
}
|
|
|
|
[Benchmark]
|
|
public void BenchReadCore()
|
|
{
|
|
_inputStream.Position = 0;
|
|
ReadPrimitiveCore(_inputStream, out string _);
|
|
}
|
|
|
|
[Benchmark]
|
|
public void BenchWriteUnsafe()
|
|
{
|
|
_outputStream.Position = 0;
|
|
WritePrimitiveUnsafe(_outputStream, _toSerialize);
|
|
}
|
|
|
|
[Benchmark]
|
|
public void BenchReadUnsafe()
|
|
{
|
|
_inputStream.Position = 0;
|
|
ReadPrimitiveUnsafe(_inputStream, out string _);
|
|
}
|
|
|
|
[Benchmark]
|
|
public void BenchWriteSlow()
|
|
{
|
|
_outputStream.Position = 0;
|
|
WritePrimitiveSlow(_outputStream, _toSerialize);
|
|
}
|
|
|
|
[Benchmark]
|
|
public void BenchReadSlow()
|
|
{
|
|
_inputStream.Position = 0;
|
|
ReadPrimitiveSlow(_inputStream, out string _);
|
|
}
|
|
|
|
public static void WritePrimitiveCore(Stream stream, string value)
|
|
{
|
|
if (value == null)
|
|
{
|
|
Primitives.WritePrimitive(stream, (uint)0);
|
|
return;
|
|
}
|
|
|
|
if (value.Length == 0)
|
|
{
|
|
Primitives.WritePrimitive(stream, (uint)1);
|
|
return;
|
|
}
|
|
|
|
Span<byte> buf = stackalloc byte[StringByteBufferLength];
|
|
|
|
var totalChars = value.Length;
|
|
var totalBytes = Encoding.UTF8.GetByteCount(value);
|
|
|
|
Primitives.WritePrimitive(stream, (uint)totalBytes + 1);
|
|
Primitives.WritePrimitive(stream, (uint)totalChars);
|
|
|
|
var totalRead = 0;
|
|
ReadOnlySpan<char> span = value;
|
|
for (;;)
|
|
{
|
|
var finalChunk = totalRead + totalChars >= totalChars;
|
|
Utf8.FromUtf16(span, buf, out var read, out var wrote, isFinalBlock: finalChunk);
|
|
stream.Write(buf.Slice(0, wrote));
|
|
totalRead += read;
|
|
if (read >= totalChars)
|
|
{
|
|
break;
|
|
}
|
|
|
|
span = span[read..];
|
|
totalChars -= read;
|
|
}
|
|
}
|
|
|
|
private static readonly SpanAction<char, (int, Stream)> _stringSpanRead = StringSpanRead;
|
|
|
|
public static void ReadPrimitiveCore(Stream stream, out string value)
|
|
{
|
|
Primitives.ReadPrimitive(stream, out uint totalBytes);
|
|
|
|
if (totalBytes == 0)
|
|
{
|
|
value = null;
|
|
return;
|
|
}
|
|
|
|
if (totalBytes == 1)
|
|
{
|
|
value = string.Empty;
|
|
return;
|
|
}
|
|
|
|
totalBytes -= 1;
|
|
|
|
Primitives.ReadPrimitive(stream, out uint totalChars);
|
|
|
|
value = string.Create((int) totalChars, ((int) totalBytes, stream), _stringSpanRead);
|
|
}
|
|
|
|
private static void StringSpanRead(Span<char> span, (int totalBytes, Stream stream) tuple)
|
|
{
|
|
Span<byte> buf = stackalloc byte[StringByteBufferLength];
|
|
|
|
// ReSharper disable VariableHidesOuterVariable
|
|
var (totalBytes, stream) = tuple;
|
|
// ReSharper restore VariableHidesOuterVariable
|
|
|
|
var totalBytesRead = 0;
|
|
var totalCharsRead = 0;
|
|
var writeBufStart = 0;
|
|
|
|
while (totalBytesRead < totalBytes)
|
|
{
|
|
var bytesLeft = totalBytes - totalBytesRead;
|
|
var bytesReadLeft = Math.Min(buf.Length, bytesLeft);
|
|
var writeSlice = buf.Slice(writeBufStart, bytesReadLeft - writeBufStart);
|
|
var bytesInBuffer = stream.Read(writeSlice);
|
|
if (bytesInBuffer == 0) throw new EndOfStreamException();
|
|
|
|
var readFromStream = bytesInBuffer + writeBufStart;
|
|
var final = readFromStream == bytesLeft;
|
|
var status = Utf8.ToUtf16(buf[..readFromStream], span[totalCharsRead..], out var bytesRead, out var charsRead, isFinalBlock: final);
|
|
|
|
totalBytesRead += bytesRead;
|
|
totalCharsRead += charsRead;
|
|
writeBufStart = 0;
|
|
|
|
if (status == OperationStatus.DestinationTooSmall)
|
|
{
|
|
// Malformed data?
|
|
throw new InvalidDataException();
|
|
}
|
|
|
|
if (status == OperationStatus.NeedMoreData)
|
|
{
|
|
// We got cut short in the middle of a multi-byte UTF-8 sequence.
|
|
// So we need to move it to the bottom of the span, then read the next bit *past* that.
|
|
// This copy should be fine because we're only ever gonna be copying up to 4 bytes
|
|
// from the end of the buffer to the start.
|
|
// So no chance of overlap.
|
|
buf[bytesRead..].CopyTo(buf);
|
|
writeBufStart = bytesReadLeft - bytesRead;
|
|
continue;
|
|
}
|
|
|
|
Debug.Assert(status == OperationStatus.Done);
|
|
}
|
|
}
|
|
|
|
public static void WritePrimitiveSlow(Stream stream, string value)
|
|
{
|
|
if (value == null)
|
|
{
|
|
Primitives.WritePrimitive(stream, (uint)0);
|
|
return;
|
|
}
|
|
else if (value.Length == 0)
|
|
{
|
|
Primitives.WritePrimitive(stream, (uint)1);
|
|
return;
|
|
}
|
|
|
|
var encoding = new UTF8Encoding(false, true);
|
|
|
|
int len = encoding.GetByteCount(value);
|
|
|
|
Primitives.WritePrimitive(stream, (uint)len + 1);
|
|
Primitives.WritePrimitive(stream, (uint)value.Length);
|
|
|
|
var buf = new byte[len];
|
|
|
|
encoding.GetBytes(value, 0, value.Length, buf, 0);
|
|
|
|
stream.Write(buf, 0, len);
|
|
}
|
|
|
|
public static void ReadPrimitiveSlow(Stream stream, out string value)
|
|
{
|
|
uint len;
|
|
Primitives.ReadPrimitive(stream, out len);
|
|
|
|
if (len == 0)
|
|
{
|
|
value = null;
|
|
return;
|
|
}
|
|
else if (len == 1)
|
|
{
|
|
value = string.Empty;
|
|
return;
|
|
}
|
|
|
|
uint totalChars;
|
|
Primitives.ReadPrimitive(stream, out totalChars);
|
|
|
|
len -= 1;
|
|
|
|
var encoding = new UTF8Encoding(false, true);
|
|
|
|
var buf = new byte[len];
|
|
|
|
int l = 0;
|
|
|
|
while (l < len)
|
|
{
|
|
int r = stream.Read(buf, l, (int)len - l);
|
|
if (r == 0)
|
|
throw new EndOfStreamException();
|
|
l += r;
|
|
}
|
|
|
|
value = encoding.GetString(buf);
|
|
}
|
|
|
|
sealed class StringHelper
|
|
{
|
|
public StringHelper()
|
|
{
|
|
this.Encoding = new UTF8Encoding(false, true);
|
|
}
|
|
|
|
Encoder m_encoder;
|
|
Decoder m_decoder;
|
|
|
|
byte[] m_byteBuffer;
|
|
char[] m_charBuffer;
|
|
|
|
public UTF8Encoding Encoding { get; private set; }
|
|
public Encoder Encoder { get { if (m_encoder == null) m_encoder = this.Encoding.GetEncoder(); return m_encoder; } }
|
|
public Decoder Decoder { get { if (m_decoder == null) m_decoder = this.Encoding.GetDecoder(); return m_decoder; } }
|
|
|
|
public byte[] ByteBuffer { get { if (m_byteBuffer == null) m_byteBuffer = new byte[StringByteBufferLength]; return m_byteBuffer; } }
|
|
public char[] CharBuffer { get { if (m_charBuffer == null) m_charBuffer = new char[StringCharBufferLength]; return m_charBuffer; } }
|
|
}
|
|
|
|
[ThreadStatic]
|
|
static StringHelper s_stringHelper;
|
|
|
|
public unsafe static void WritePrimitiveUnsafe(Stream stream, string value)
|
|
{
|
|
if (value == null)
|
|
{
|
|
Primitives.WritePrimitive(stream, (uint)0);
|
|
return;
|
|
}
|
|
else if (value.Length == 0)
|
|
{
|
|
Primitives.WritePrimitive(stream, (uint)1);
|
|
return;
|
|
}
|
|
|
|
var helper = s_stringHelper;
|
|
if (helper == null)
|
|
s_stringHelper = helper = new StringHelper();
|
|
|
|
var encoder = helper.Encoder;
|
|
var buf = helper.ByteBuffer;
|
|
|
|
int totalChars = value.Length;
|
|
int totalBytes;
|
|
|
|
fixed (char* ptr = value)
|
|
totalBytes = encoder.GetByteCount(ptr, totalChars, true);
|
|
|
|
Primitives.WritePrimitive(stream, (uint)totalBytes + 1);
|
|
Primitives.WritePrimitive(stream, (uint)totalChars);
|
|
|
|
int p = 0;
|
|
bool completed = false;
|
|
|
|
while (completed == false)
|
|
{
|
|
int charsConverted;
|
|
int bytesConverted;
|
|
|
|
fixed (char* src = value)
|
|
fixed (byte* dst = buf)
|
|
{
|
|
encoder.Convert(src + p, totalChars - p, dst, buf.Length, true,
|
|
out charsConverted, out bytesConverted, out completed);
|
|
}
|
|
|
|
stream.Write(buf, 0, bytesConverted);
|
|
|
|
p += charsConverted;
|
|
}
|
|
}
|
|
|
|
public static void ReadPrimitiveUnsafe(Stream stream, out string value)
|
|
{
|
|
uint totalBytes;
|
|
Primitives.ReadPrimitive(stream, out totalBytes);
|
|
|
|
if (totalBytes == 0)
|
|
{
|
|
value = null;
|
|
return;
|
|
}
|
|
else if (totalBytes == 1)
|
|
{
|
|
value = string.Empty;
|
|
return;
|
|
}
|
|
|
|
totalBytes -= 1;
|
|
|
|
uint totalChars;
|
|
Primitives.ReadPrimitive(stream, out totalChars);
|
|
|
|
var helper = s_stringHelper;
|
|
if (helper == null)
|
|
s_stringHelper = helper = new StringHelper();
|
|
|
|
var decoder = helper.Decoder;
|
|
var buf = helper.ByteBuffer;
|
|
char[] chars;
|
|
if (totalChars <= StringCharBufferLength)
|
|
chars = helper.CharBuffer;
|
|
else
|
|
chars = new char[totalChars];
|
|
|
|
int streamBytesLeft = (int)totalBytes;
|
|
|
|
int cp = 0;
|
|
|
|
while (streamBytesLeft > 0)
|
|
{
|
|
int bytesInBuffer = stream.Read(buf, 0, Math.Min(buf.Length, streamBytesLeft));
|
|
if (bytesInBuffer == 0)
|
|
throw new EndOfStreamException();
|
|
|
|
streamBytesLeft -= bytesInBuffer;
|
|
bool flush = streamBytesLeft == 0 ? true : false;
|
|
|
|
bool completed = false;
|
|
|
|
int p = 0;
|
|
|
|
while (completed == false)
|
|
{
|
|
int charsConverted;
|
|
int bytesConverted;
|
|
|
|
decoder.Convert(buf, p, bytesInBuffer - p,
|
|
chars, cp, (int)totalChars - cp,
|
|
flush,
|
|
out bytesConverted, out charsConverted, out completed);
|
|
|
|
p += bytesConverted;
|
|
cp += charsConverted;
|
|
}
|
|
}
|
|
|
|
value = new string(chars, 0, (int)totalChars);
|
|
}
|
|
}
|
|
}
|