NetSerializerStringBenchmark.cs 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431
  1. using System;
  2. using System.Buffers;
  3. using System.Diagnostics;
  4. using System.IO;
  5. using System.Text;
  6. using System.Text.Unicode;
  7. using BenchmarkDotNet.Attributes;
  8. using Lidgren.Network;
  9. using NetSerializer;
  10. using Robust.Shared.Analyzers;
  11. namespace Content.Benchmarks
  12. {
  13. // Code for the *Slow and *Unsafe implementations taken from NetSerializer, licensed under the MIT license.
  14. [MemoryDiagnoser]
  15. [Virtual]
  16. public class NetSerializerStringBenchmark
  17. {
  18. private const int StringByteBufferLength = 256;
  19. private const int StringCharBufferLength = 128;
  20. private string _toSerialize;
  21. [Params(8, 64, 256, 1024)]
  22. public int StringLength { get; set; }
  23. private readonly MemoryStream _outputStream = new(2048);
  24. private readonly MemoryStream _inputStream = new(2048);
  25. [GlobalSetup]
  26. public void Setup()
  27. {
  28. Span<byte> buf = stackalloc byte[StringLength / 2];
  29. new Random().NextBytes(buf);
  30. _toSerialize = NetUtility.ToHexString(buf);
  31. Primitives.WritePrimitive(_inputStream, _toSerialize);
  32. }
  33. [Benchmark]
  34. public void BenchWriteCore()
  35. {
  36. _outputStream.Position = 0;
  37. WritePrimitiveCore(_outputStream, _toSerialize);
  38. }
  39. [Benchmark]
  40. public void BenchReadCore()
  41. {
  42. _inputStream.Position = 0;
  43. ReadPrimitiveCore(_inputStream, out _);
  44. }
  45. [Benchmark]
  46. public void BenchWriteUnsafe()
  47. {
  48. _outputStream.Position = 0;
  49. WritePrimitiveUnsafe(_outputStream, _toSerialize);
  50. }
  51. [Benchmark]
  52. public void BenchReadUnsafe()
  53. {
  54. _inputStream.Position = 0;
  55. ReadPrimitiveUnsafe(_inputStream, out _);
  56. }
  57. [Benchmark]
  58. public void BenchWriteSlow()
  59. {
  60. _outputStream.Position = 0;
  61. WritePrimitiveSlow(_outputStream, _toSerialize);
  62. }
  63. [Benchmark]
  64. public void BenchReadSlow()
  65. {
  66. _inputStream.Position = 0;
  67. ReadPrimitiveSlow(_inputStream, out _);
  68. }
  69. public static void WritePrimitiveCore(Stream stream, string value)
  70. {
  71. if (value == null)
  72. {
  73. Primitives.WritePrimitive(stream, (uint) 0);
  74. return;
  75. }
  76. if (value.Length == 0)
  77. {
  78. Primitives.WritePrimitive(stream, (uint) 1);
  79. return;
  80. }
  81. Span<byte> buf = stackalloc byte[StringByteBufferLength];
  82. var totalChars = value.Length;
  83. var totalBytes = Encoding.UTF8.GetByteCount(value);
  84. Primitives.WritePrimitive(stream, (uint) totalBytes + 1);
  85. Primitives.WritePrimitive(stream, (uint) totalChars);
  86. var totalRead = 0;
  87. ReadOnlySpan<char> span = value;
  88. while (true)
  89. {
  90. var finalChunk = totalRead + totalChars >= totalChars;
  91. Utf8.FromUtf16(span, buf, out var read, out var wrote, isFinalBlock: finalChunk);
  92. stream.Write(buf[0..wrote]);
  93. totalRead += read;
  94. if (read >= totalChars)
  95. {
  96. break;
  97. }
  98. span = span[read..];
  99. totalChars -= read;
  100. }
  101. }
  102. public static void ReadPrimitiveCore(Stream stream, out string value)
  103. {
  104. Primitives.ReadPrimitive(stream, out uint totalBytes);
  105. if (totalBytes == 0)
  106. {
  107. value = null;
  108. return;
  109. }
  110. if (totalBytes == 1)
  111. {
  112. value = string.Empty;
  113. return;
  114. }
  115. totalBytes -= 1;
  116. Primitives.ReadPrimitive(stream, out uint totalChars);
  117. value = string.Create((int) totalChars, ((int) totalBytes, stream), StringSpanRead);
  118. }
  119. private static void StringSpanRead(Span<char> span, (int totalBytes, Stream stream) tuple)
  120. {
  121. Span<byte> buf = stackalloc byte[StringByteBufferLength];
  122. // ReSharper disable VariableHidesOuterVariable
  123. var (totalBytes, stream) = tuple;
  124. // ReSharper restore VariableHidesOuterVariable
  125. var totalBytesRead = 0;
  126. var totalCharsRead = 0;
  127. var writeBufStart = 0;
  128. while (totalBytesRead < totalBytes)
  129. {
  130. var bytesLeft = totalBytes - totalBytesRead;
  131. var bytesReadLeft = Math.Min(buf.Length, bytesLeft);
  132. var writeSlice = buf[writeBufStart..(bytesReadLeft - writeBufStart)];
  133. var bytesInBuffer = stream.Read(writeSlice);
  134. if (bytesInBuffer == 0) throw new EndOfStreamException();
  135. var readFromStream = bytesInBuffer + writeBufStart;
  136. var final = readFromStream == bytesLeft;
  137. var status = Utf8.ToUtf16(buf[..readFromStream], span[totalCharsRead..], out var bytesRead, out var charsRead, isFinalBlock: final);
  138. totalBytesRead += bytesRead;
  139. totalCharsRead += charsRead;
  140. writeBufStart = 0;
  141. if (status == OperationStatus.DestinationTooSmall)
  142. {
  143. // Malformed data?
  144. throw new InvalidDataException();
  145. }
  146. if (status == OperationStatus.NeedMoreData)
  147. {
  148. // We got cut short in the middle of a multi-byte UTF-8 sequence.
  149. // So we need to move it to the bottom of the span, then read the next bit *past* that.
  150. // This copy should be fine because we're only ever gonna be copying up to 4 bytes
  151. // from the end of the buffer to the start.
  152. // So no chance of overlap.
  153. buf[bytesRead..].CopyTo(buf);
  154. writeBufStart = bytesReadLeft - bytesRead;
  155. continue;
  156. }
  157. Debug.Assert(status == OperationStatus.Done);
  158. }
  159. }
  160. public static void WritePrimitiveSlow(Stream stream, string value)
  161. {
  162. if (value == null)
  163. {
  164. Primitives.WritePrimitive(stream, (uint) 0);
  165. return;
  166. }
  167. else if (value.Length == 0)
  168. {
  169. Primitives.WritePrimitive(stream, (uint) 1);
  170. return;
  171. }
  172. var encoding = new UTF8Encoding(false, true);
  173. var len = encoding.GetByteCount(value);
  174. Primitives.WritePrimitive(stream, (uint) len + 1);
  175. Primitives.WritePrimitive(stream, (uint) value.Length);
  176. var buf = new byte[len];
  177. encoding.GetBytes(value, 0, value.Length, buf, 0);
  178. stream.Write(buf, 0, len);
  179. }
  180. public static void ReadPrimitiveSlow(Stream stream, out string value)
  181. {
  182. Primitives.ReadPrimitive(stream, out uint len);
  183. if (len == 0)
  184. {
  185. value = null;
  186. return;
  187. }
  188. else if (len == 1)
  189. {
  190. value = string.Empty;
  191. return;
  192. }
  193. Primitives.ReadPrimitive(stream, out uint _);
  194. len -= 1;
  195. var encoding = new UTF8Encoding(false, true);
  196. var buf = new byte[len];
  197. var l = 0;
  198. while (l < len)
  199. {
  200. var r = stream.Read(buf, l, (int) len - l);
  201. if (r == 0)
  202. throw new EndOfStreamException();
  203. l += r;
  204. }
  205. value = encoding.GetString(buf);
  206. }
  207. private sealed class StringHelper
  208. {
  209. public StringHelper()
  210. {
  211. Encoding = new UTF8Encoding(false, true);
  212. }
  213. private Encoder _encoder;
  214. private Decoder _decoder;
  215. private byte[] _byteBuffer;
  216. private char[] _charBuffer;
  217. public UTF8Encoding Encoding { get; private set; }
  218. public Encoder Encoder
  219. {
  220. get
  221. {
  222. _encoder ??= Encoding.GetEncoder();
  223. return _encoder;
  224. }
  225. }
  226. public Decoder Decoder
  227. {
  228. get
  229. {
  230. _decoder ??= Encoding.GetDecoder();
  231. return _decoder;
  232. }
  233. }
  234. public byte[] ByteBuffer
  235. {
  236. get
  237. {
  238. _byteBuffer ??= new byte[StringByteBufferLength];
  239. return _byteBuffer;
  240. }
  241. }
  242. public char[] CharBuffer
  243. {
  244. get
  245. {
  246. _charBuffer ??= new char[StringCharBufferLength];
  247. return _charBuffer;
  248. }
  249. }
  250. }
  251. [ThreadStatic]
  252. private static StringHelper _stringHelper;
  253. public static unsafe void WritePrimitiveUnsafe(Stream stream, string value)
  254. {
  255. if (value == null)
  256. {
  257. Primitives.WritePrimitive(stream, (uint) 0);
  258. return;
  259. }
  260. else if (value.Length == 0)
  261. {
  262. Primitives.WritePrimitive(stream, (uint) 1);
  263. return;
  264. }
  265. var helper = _stringHelper;
  266. if (helper == null)
  267. _stringHelper = helper = new StringHelper();
  268. var encoder = helper.Encoder;
  269. var buf = helper.ByteBuffer;
  270. var totalChars = value.Length;
  271. int totalBytes;
  272. fixed (char* ptr = value)
  273. totalBytes = encoder.GetByteCount(ptr, totalChars, true);
  274. Primitives.WritePrimitive(stream, (uint) totalBytes + 1);
  275. Primitives.WritePrimitive(stream, (uint) totalChars);
  276. var p = 0;
  277. var completed = false;
  278. while (completed == false)
  279. {
  280. int charsConverted;
  281. int bytesConverted;
  282. fixed (char* src = value)
  283. fixed (byte* dst = buf)
  284. {
  285. encoder.Convert(src + p, totalChars - p, dst, buf.Length, true,
  286. out charsConverted, out bytesConverted, out completed);
  287. }
  288. stream.Write(buf, 0, bytesConverted);
  289. p += charsConverted;
  290. }
  291. }
  292. public static void ReadPrimitiveUnsafe(Stream stream, out string value)
  293. {
  294. Primitives.ReadPrimitive(stream, out uint totalBytes);
  295. if (totalBytes == 0)
  296. {
  297. value = null;
  298. return;
  299. }
  300. else if (totalBytes == 1)
  301. {
  302. value = string.Empty;
  303. return;
  304. }
  305. totalBytes -= 1;
  306. Primitives.ReadPrimitive(stream, out uint totalChars);
  307. var helper = _stringHelper;
  308. if (helper == null)
  309. _stringHelper = helper = new StringHelper();
  310. var decoder = helper.Decoder;
  311. var buf = helper.ByteBuffer;
  312. char[] chars;
  313. if (totalChars <= StringCharBufferLength)
  314. chars = helper.CharBuffer;
  315. else
  316. chars = new char[totalChars];
  317. var streamBytesLeft = (int) totalBytes;
  318. var cp = 0;
  319. while (streamBytesLeft > 0)
  320. {
  321. var bytesInBuffer = stream.Read(buf, 0, Math.Min(buf.Length, streamBytesLeft));
  322. if (bytesInBuffer == 0)
  323. throw new EndOfStreamException();
  324. streamBytesLeft -= bytesInBuffer;
  325. var flush = streamBytesLeft == 0;
  326. var completed = false;
  327. var p = 0;
  328. while (completed == false)
  329. {
  330. decoder.Convert(
  331. buf,
  332. p,
  333. bytesInBuffer - p,
  334. chars,
  335. cp,
  336. (int) totalChars - cp,
  337. flush,
  338. out var bytesConverted,
  339. out var charsConverted,
  340. out completed
  341. );
  342. p += bytesConverted;
  343. cp += charsConverted;
  344. }
  345. }
  346. value = new string(chars, 0, (int) totalChars);
  347. }
  348. }
  349. }