ParseRawPrimitivesBenchmark.cs 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467
  1. #region Copyright notice and license
  2. // Protocol Buffers - Google's data interchange format
  3. // Copyright 2019 Google Inc. All rights reserved.
  4. // https://github.com/protocolbuffers/protobuf
  5. //
  6. // Redistribution and use in source and binary forms, with or without
  7. // modification, are permitted provided that the following conditions are
  8. // met:
  9. //
  10. // * Redistributions of source code must retain the above copyright
  11. // notice, this list of conditions and the following disclaimer.
  12. // * Redistributions in binary form must reproduce the above
  13. // copyright notice, this list of conditions and the following disclaimer
  14. // in the documentation and/or other materials provided with the
  15. // distribution.
  16. // * Neither the name of Google Inc. nor the names of its
  17. // contributors may be used to endorse or promote products derived from
  18. // this software without specific prior written permission.
  19. //
  20. // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
  21. // "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
  22. // LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
  23. // A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
  24. // OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
  25. // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
  26. // LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
  27. // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
  28. // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
  29. // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
  30. // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  31. #endregion
  32. using BenchmarkDotNet.Attributes;
  33. using System;
  34. using System.Buffers.Binary;
  35. using System.Collections.Generic;
  36. using System.IO;
  37. using System.Buffers;
  38. namespace Google.Protobuf.Benchmarks
  39. {
  40. /// <summary>
  41. /// Benchmarks throughput when parsing raw primitives.
  42. /// </summary>
  43. [MemoryDiagnoser]
  44. public class ParseRawPrimitivesBenchmark
  45. {
  46. // key is the encodedSize of varint values
  47. Dictionary<int, byte[]> varintInputBuffers;
  48. byte[] doubleInputBuffer;
  49. byte[] floatInputBuffer;
  50. byte[] fixedIntInputBuffer;
  51. // key is the encodedSize of string values
  52. Dictionary<int, byte[]> stringInputBuffers;
  53. Random random = new Random(417384220); // random but deterministic seed
  54. public IEnumerable<int> StringEncodedSizes => new[] { 1, 4, 10, 105, 10080 };
  55. [GlobalSetup]
  56. public void GlobalSetup()
  57. {
  58. // add some extra values that we won't read just to make sure we are far enough from the end of the buffer
  59. // which allows the parser fastpath to always kick in.
  60. const int paddingValueCount = 100;
  61. varintInputBuffers = new Dictionary<int, byte[]>();
  62. for (int encodedSize = 1; encodedSize <= 10; encodedSize++)
  63. {
  64. byte[] buffer = CreateBufferWithRandomVarints(random, BytesToParse / encodedSize, encodedSize, paddingValueCount);
  65. varintInputBuffers.Add(encodedSize, buffer);
  66. }
  67. doubleInputBuffer = CreateBufferWithRandomDoubles(random, BytesToParse / sizeof(double), paddingValueCount);
  68. floatInputBuffer = CreateBufferWithRandomFloats(random, BytesToParse / sizeof(float), paddingValueCount);
  69. fixedIntInputBuffer = CreateBufferWithRandomData(random, BytesToParse / sizeof(long), sizeof(long), paddingValueCount);
  70. stringInputBuffers = new Dictionary<int, byte[]>();
  71. foreach(var encodedSize in StringEncodedSizes)
  72. {
  73. byte[] buffer = CreateBufferWithStrings(BytesToParse / encodedSize, encodedSize, encodedSize < 10 ? 10 : 1 );
  74. stringInputBuffers.Add(encodedSize, buffer);
  75. }
  76. }
  77. // Total number of bytes that each benchmark will parse.
  78. // Measuring the time taken to parse buffer of given size makes it easier to compare parsing speed for different
  79. // types and makes it easy to calculate the througput (in MB/s)
  80. // 10800 bytes is chosen because it is divisible by all possible encoded sizes for all primitive types {1..10}
  81. [Params(10080)]
  82. public int BytesToParse { get; set; }
  83. [Benchmark]
  84. [Arguments(1)]
  85. [Arguments(2)]
  86. [Arguments(3)]
  87. [Arguments(4)]
  88. [Arguments(5)]
  89. public int ParseRawVarint32_CodedInputStream(int encodedSize)
  90. {
  91. CodedInputStream cis = new CodedInputStream(varintInputBuffers[encodedSize]);
  92. int sum = 0;
  93. for (int i = 0; i < BytesToParse / encodedSize; i++)
  94. {
  95. sum += cis.ReadInt32();
  96. }
  97. return sum;
  98. }
  99. [Benchmark]
  100. [Arguments(1)]
  101. [Arguments(2)]
  102. [Arguments(3)]
  103. [Arguments(4)]
  104. [Arguments(5)]
  105. public int ParseRawVarint32_ParseContext(int encodedSize)
  106. {
  107. InitializeParseContext(varintInputBuffers[encodedSize], out ParseContext ctx);
  108. int sum = 0;
  109. for (int i = 0; i < BytesToParse / encodedSize; i++)
  110. {
  111. sum += ctx.ReadInt32();
  112. }
  113. return sum;
  114. }
  115. [Benchmark]
  116. [Arguments(1)]
  117. [Arguments(2)]
  118. [Arguments(3)]
  119. [Arguments(4)]
  120. [Arguments(5)]
  121. [Arguments(6)]
  122. [Arguments(7)]
  123. [Arguments(8)]
  124. [Arguments(9)]
  125. [Arguments(10)]
  126. public long ParseRawVarint64_CodedInputStream(int encodedSize)
  127. {
  128. CodedInputStream cis = new CodedInputStream(varintInputBuffers[encodedSize]);
  129. long sum = 0;
  130. for (int i = 0; i < BytesToParse / encodedSize; i++)
  131. {
  132. sum += cis.ReadInt64();
  133. }
  134. return sum;
  135. }
  136. [Benchmark]
  137. [Arguments(1)]
  138. [Arguments(2)]
  139. [Arguments(3)]
  140. [Arguments(4)]
  141. [Arguments(5)]
  142. [Arguments(6)]
  143. [Arguments(7)]
  144. [Arguments(8)]
  145. [Arguments(9)]
  146. [Arguments(10)]
  147. public long ParseRawVarint64_ParseContext(int encodedSize)
  148. {
  149. InitializeParseContext(varintInputBuffers[encodedSize], out ParseContext ctx);
  150. long sum = 0;
  151. for (int i = 0; i < BytesToParse / encodedSize; i++)
  152. {
  153. sum += ctx.ReadInt64();
  154. }
  155. return sum;
  156. }
  157. [Benchmark]
  158. public uint ParseFixed32_CodedInputStream()
  159. {
  160. const int encodedSize = sizeof(uint);
  161. CodedInputStream cis = new CodedInputStream(fixedIntInputBuffer);
  162. uint sum = 0;
  163. for (uint i = 0; i < BytesToParse / encodedSize; i++)
  164. {
  165. sum += cis.ReadFixed32();
  166. }
  167. return sum;
  168. }
  169. [Benchmark]
  170. public uint ParseFixed32_ParseContext()
  171. {
  172. const int encodedSize = sizeof(uint);
  173. InitializeParseContext(fixedIntInputBuffer, out ParseContext ctx);
  174. uint sum = 0;
  175. for (uint i = 0; i < BytesToParse / encodedSize; i++)
  176. {
  177. sum += ctx.ReadFixed32();
  178. }
  179. return sum;
  180. }
  181. [Benchmark]
  182. public ulong ParseFixed64_CodedInputStream()
  183. {
  184. const int encodedSize = sizeof(ulong);
  185. CodedInputStream cis = new CodedInputStream(fixedIntInputBuffer);
  186. ulong sum = 0;
  187. for (int i = 0; i < BytesToParse / encodedSize; i++)
  188. {
  189. sum += cis.ReadFixed64();
  190. }
  191. return sum;
  192. }
  193. [Benchmark]
  194. public ulong ParseFixed64_ParseContext()
  195. {
  196. const int encodedSize = sizeof(ulong);
  197. InitializeParseContext(fixedIntInputBuffer, out ParseContext ctx);
  198. ulong sum = 0;
  199. for (int i = 0; i < BytesToParse / encodedSize; i++)
  200. {
  201. sum += ctx.ReadFixed64();
  202. }
  203. return sum;
  204. }
  205. [Benchmark]
  206. public float ParseRawFloat_CodedInputStream()
  207. {
  208. const int encodedSize = sizeof(float);
  209. CodedInputStream cis = new CodedInputStream(floatInputBuffer);
  210. float sum = 0;
  211. for (int i = 0; i < BytesToParse / encodedSize; i++)
  212. {
  213. sum += cis.ReadFloat();
  214. }
  215. return sum;
  216. }
  217. [Benchmark]
  218. public float ParseRawFloat_ParseContext()
  219. {
  220. const int encodedSize = sizeof(float);
  221. InitializeParseContext(floatInputBuffer, out ParseContext ctx);
  222. float sum = 0;
  223. for (int i = 0; i < BytesToParse / encodedSize; i++)
  224. {
  225. sum += ctx.ReadFloat();
  226. }
  227. return sum;
  228. }
  229. [Benchmark]
  230. public double ParseRawDouble_CodedInputStream()
  231. {
  232. const int encodedSize = sizeof(double);
  233. CodedInputStream cis = new CodedInputStream(doubleInputBuffer);
  234. double sum = 0;
  235. for (int i = 0; i < BytesToParse / encodedSize; i++)
  236. {
  237. sum += cis.ReadDouble();
  238. }
  239. return sum;
  240. }
  241. [Benchmark]
  242. public double ParseRawDouble_ParseContext()
  243. {
  244. const int encodedSize = sizeof(double);
  245. InitializeParseContext(doubleInputBuffer, out ParseContext ctx);
  246. double sum = 0;
  247. for (int i = 0; i < BytesToParse / encodedSize; i++)
  248. {
  249. sum += ctx.ReadDouble();
  250. }
  251. return sum;
  252. }
  253. [Benchmark]
  254. [ArgumentsSource(nameof(StringEncodedSizes))]
  255. public int ParseString_CodedInputStream(int encodedSize)
  256. {
  257. CodedInputStream cis = new CodedInputStream(stringInputBuffers[encodedSize]);
  258. int sum = 0;
  259. for (int i = 0; i < BytesToParse / encodedSize; i++)
  260. {
  261. sum += cis.ReadString().Length;
  262. }
  263. return sum;
  264. }
  265. [Benchmark]
  266. [ArgumentsSource(nameof(StringEncodedSizes))]
  267. public int ParseString_ParseContext(int encodedSize)
  268. {
  269. InitializeParseContext(stringInputBuffers[encodedSize], out ParseContext ctx);
  270. int sum = 0;
  271. for (int i = 0; i < BytesToParse / encodedSize; i++)
  272. {
  273. sum += ctx.ReadString().Length;
  274. }
  275. return sum;
  276. }
  277. [Benchmark]
  278. [ArgumentsSource(nameof(StringEncodedSizes))]
  279. public int ParseBytes_CodedInputStream(int encodedSize)
  280. {
  281. CodedInputStream cis = new CodedInputStream(stringInputBuffers[encodedSize]);
  282. int sum = 0;
  283. for (int i = 0; i < BytesToParse / encodedSize; i++)
  284. {
  285. sum += cis.ReadBytes().Length;
  286. }
  287. return sum;
  288. }
  289. [Benchmark]
  290. [ArgumentsSource(nameof(StringEncodedSizes))]
  291. public int ParseBytes_ParseContext(int encodedSize)
  292. {
  293. InitializeParseContext(stringInputBuffers[encodedSize], out ParseContext ctx);
  294. int sum = 0;
  295. for (int i = 0; i < BytesToParse / encodedSize; i++)
  296. {
  297. sum += ctx.ReadBytes().Length;
  298. }
  299. return sum;
  300. }
  301. private static void InitializeParseContext(byte[] buffer, out ParseContext ctx)
  302. {
  303. ParseContext.Initialize(new ReadOnlySequence<byte>(buffer), out ctx);
  304. }
  305. private static byte[] CreateBufferWithRandomVarints(Random random, int valueCount, int encodedSize, int paddingValueCount)
  306. {
  307. MemoryStream ms = new MemoryStream();
  308. CodedOutputStream cos = new CodedOutputStream(ms);
  309. for (int i = 0; i < valueCount + paddingValueCount; i++)
  310. {
  311. cos.WriteUInt64(RandomUnsignedVarint(random, encodedSize, false));
  312. }
  313. cos.Flush();
  314. var buffer = ms.ToArray();
  315. if (buffer.Length != encodedSize * (valueCount + paddingValueCount))
  316. {
  317. throw new InvalidOperationException($"Unexpected output buffer length {buffer.Length}");
  318. }
  319. return buffer;
  320. }
  321. private static byte[] CreateBufferWithRandomFloats(Random random, int valueCount, int paddingValueCount)
  322. {
  323. MemoryStream ms = new MemoryStream();
  324. CodedOutputStream cos = new CodedOutputStream(ms);
  325. for (int i = 0; i < valueCount + paddingValueCount; i++)
  326. {
  327. cos.WriteFloat((float)random.NextDouble());
  328. }
  329. cos.Flush();
  330. var buffer = ms.ToArray();
  331. return buffer;
  332. }
  333. private static byte[] CreateBufferWithRandomDoubles(Random random, int valueCount, int paddingValueCount)
  334. {
  335. MemoryStream ms = new MemoryStream();
  336. CodedOutputStream cos = new CodedOutputStream(ms);
  337. for (int i = 0; i < valueCount + paddingValueCount; i++)
  338. {
  339. cos.WriteDouble(random.NextDouble());
  340. }
  341. cos.Flush();
  342. var buffer = ms.ToArray();
  343. return buffer;
  344. }
  345. private static byte[] CreateBufferWithRandomData(Random random, int valueCount, int encodedSize, int paddingValueCount)
  346. {
  347. int bufferSize = (valueCount + paddingValueCount) * encodedSize;
  348. byte[] buffer = new byte[bufferSize];
  349. random.NextBytes(buffer);
  350. return buffer;
  351. }
  352. /// <summary>
  353. /// Generate a random value that will take exactly "encodedSize" bytes when varint-encoded.
  354. /// </summary>
  355. public static ulong RandomUnsignedVarint(Random random, int encodedSize, bool fitsIn32Bits)
  356. {
  357. Span<byte> randomBytesBuffer = stackalloc byte[8];
  358. if (encodedSize < 1 || encodedSize > 10 || (fitsIn32Bits && encodedSize > 5))
  359. {
  360. throw new ArgumentException("Illegal encodedSize value requested", nameof(encodedSize));
  361. }
  362. const int bitsPerByte = 7;
  363. ulong result = 0;
  364. while (true)
  365. {
  366. random.NextBytes(randomBytesBuffer);
  367. ulong randomValue = BinaryPrimitives.ReadUInt64LittleEndian(randomBytesBuffer);
  368. // only use the number of random bits we need
  369. ulong bitmask = encodedSize < 10 ? ((1UL << (encodedSize * bitsPerByte)) - 1) : ulong.MaxValue;
  370. result = randomValue & bitmask;
  371. if (fitsIn32Bits)
  372. {
  373. // make sure the resulting value is representable by a uint.
  374. result &= uint.MaxValue;
  375. }
  376. if (encodedSize == 10)
  377. {
  378. // for 10-byte values the highest bit always needs to be set (7*9=63)
  379. result |= ulong.MaxValue;
  380. break;
  381. }
  382. // some random values won't require the full "encodedSize" bytes, check that at least
  383. // one of the top 7 bits is set. Retrying is fine since it only happens rarely
  384. if (encodedSize == 1 || (result & (0x7FUL << ((encodedSize - 1) * bitsPerByte))) != 0)
  385. {
  386. break;
  387. }
  388. }
  389. return result;
  390. }
  391. private static byte[] CreateBufferWithStrings(int valueCount, int encodedSize, int paddingValueCount)
  392. {
  393. var str = CreateStringWithEncodedSize(encodedSize);
  394. MemoryStream ms = new MemoryStream();
  395. CodedOutputStream cos = new CodedOutputStream(ms);
  396. for (int i = 0; i < valueCount + paddingValueCount; i++)
  397. {
  398. cos.WriteString(str);
  399. }
  400. cos.Flush();
  401. var buffer = ms.ToArray();
  402. if (buffer.Length != encodedSize * (valueCount + paddingValueCount))
  403. {
  404. throw new InvalidOperationException($"Unexpected output buffer length {buffer.Length}");
  405. }
  406. return buffer;
  407. }
  408. public static string CreateStringWithEncodedSize(int encodedSize)
  409. {
  410. var str = new string('a', encodedSize);
  411. while (CodedOutputStream.ComputeStringSize(str) > encodedSize)
  412. {
  413. str = str.Substring(1);
  414. }
  415. if (CodedOutputStream.ComputeStringSize(str) != encodedSize)
  416. {
  417. throw new InvalidOperationException($"Generated string with wrong encodedSize");
  418. }
  419. return str;
  420. }
  421. }
  422. }