Files
tbd-station-14/Content.Benchmarks/NetSerializerStringBenchmark.cs
2020-07-30 23:39:47 +02:00

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 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 MemoryStream(2048);
private readonly MemoryStream _inputStream = new MemoryStream(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);
}
}
}