NetSerializer string benchmarks.
This commit is contained in:
402
Content.Benchmarks/NetSerializerStringBenchmark.cs
Normal file
402
Content.Benchmarks/NetSerializerStringBenchmark.cs
Normal file
@@ -0,0 +1,402 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user