diff --git a/Content.Benchmarks/NetSerializerStringBenchmark.cs b/Content.Benchmarks/NetSerializerStringBenchmark.cs new file mode 100644 index 0000000000..ba42c3f694 --- /dev/null +++ b/Content.Benchmarks/NetSerializerStringBenchmark.cs @@ -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 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 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 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 _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 span, (int totalBytes, Stream stream) tuple) + { + Span 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); + } + } +}