diff --git a/Makefile.am b/Makefile.am index b4e6ebd886..f3c688a0d3 100644 --- a/Makefile.am +++ b/Makefile.am @@ -88,6 +88,7 @@ csharp_EXTRA_DIST= \ csharp/src/Google.Protobuf.Benchmarks/BenchmarkMessage1Proto3.cs \ csharp/src/Google.Protobuf.Benchmarks/Benchmarks.cs \ csharp/src/Google.Protobuf.Benchmarks/Google.Protobuf.Benchmarks.csproj \ + csharp/src/Google.Protobuf.Benchmarks/ParseRawPrimitivesBenchmark.cs \ csharp/src/Google.Protobuf.Benchmarks/Program.cs \ csharp/src/Google.Protobuf.Benchmarks/SerializationBenchmark.cs \ csharp/src/Google.Protobuf.Benchmarks/SerializationConfig.cs \ diff --git a/csharp/src/Google.Protobuf.Benchmarks/ParseRawPrimitivesBenchmark.cs b/csharp/src/Google.Protobuf.Benchmarks/ParseRawPrimitivesBenchmark.cs new file mode 100644 index 0000000000..44dde2ff8c --- /dev/null +++ b/csharp/src/Google.Protobuf.Benchmarks/ParseRawPrimitivesBenchmark.cs @@ -0,0 +1,265 @@ +#region Copyright notice and license +// Protocol Buffers - Google's data interchange format +// Copyright 2019 Google Inc. All rights reserved. +// https://github.com/protocolbuffers/protobuf +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +#endregion + +using BenchmarkDotNet.Attributes; +using System; +using System.Buffers.Binary; +using System.Collections.Generic; +using System.IO; + +namespace Google.Protobuf.Benchmarks +{ + /// + /// Benchmarks throughput when parsing raw primitives. + /// + [MemoryDiagnoser] + public class ParseRawPrimitivesBenchmark + { + // key is the encodedSize of varint values + Dictionary varintInputBuffers; + + byte[] doubleInputBuffer; + byte[] floatInputBuffer; + byte[] fixedIntInputBuffer; + + Random random = new Random(417384220); // random but deterministic seed + + [GlobalSetup] + public void GlobalSetup() + { + // add some extra values that we won't read just to make sure we are far enough from the end of the buffer + // which allows the parser fastpath to always kick in. + const int paddingValueCount = 100; + + varintInputBuffers = new Dictionary(); + for (int encodedSize = 1; encodedSize <= 10; encodedSize++) + { + byte[] buffer = CreateBufferWithRandomVarints(random, BytesToParse / encodedSize, encodedSize, paddingValueCount); + varintInputBuffers.Add(encodedSize, buffer); + } + + doubleInputBuffer = CreateBufferWithRandomDoubles(random, BytesToParse / sizeof(double), paddingValueCount); + floatInputBuffer = CreateBufferWithRandomFloats(random, BytesToParse / sizeof(float), paddingValueCount); + fixedIntInputBuffer = CreateBufferWithRandomData(random, BytesToParse / sizeof(long), sizeof(long), paddingValueCount); + } + + // Total number of bytes that each benchmark will parse. + // Measuring the time taken to parse buffer of given size makes it easier to compare parsing speed for different + // types and makes it easy to calculate the througput (in MB/s) + // 10800 bytes is chosen because it is divisible by all possible encoded sizes for all primitive types {1..10} + [Params(10080)] + public int BytesToParse { get; set; } + + [Benchmark] + [Arguments(1)] + [Arguments(2)] + [Arguments(3)] + [Arguments(4)] + [Arguments(5)] + public int ParseRawVarint32(int encodedSize) + { + CodedInputStream cis = new CodedInputStream(varintInputBuffers[encodedSize]); + int sum = 0; + for (int i = 0; i < BytesToParse / encodedSize; i++) + { + sum += cis.ReadInt32(); + } + return sum; + } + + [Benchmark] + [Arguments(1)] + [Arguments(2)] + [Arguments(3)] + [Arguments(4)] + [Arguments(5)] + [Arguments(6)] + [Arguments(7)] + [Arguments(8)] + [Arguments(9)] + [Arguments(10)] + public long ParseRawVarint64(int encodedSize) + { + CodedInputStream cis = new CodedInputStream(varintInputBuffers[encodedSize]); + long sum = 0; + for (int i = 0; i < BytesToParse / encodedSize; i++) + { + sum += cis.ReadInt64(); + } + return sum; + } + + [Benchmark] + public uint ParseFixed32() + { + const int encodedSize = sizeof(uint); + CodedInputStream cis = new CodedInputStream(fixedIntInputBuffer); + uint sum = 0; + for (uint i = 0; i < BytesToParse / encodedSize; i++) + { + sum += cis.ReadFixed32(); + } + return sum; + } + + [Benchmark] + public ulong ParseFixed64() + { + const int encodedSize = sizeof(ulong); + CodedInputStream cis = new CodedInputStream(fixedIntInputBuffer); + ulong sum = 0; + for (int i = 0; i < BytesToParse / encodedSize; i++) + { + sum += cis.ReadFixed64(); + } + return sum; + } + + [Benchmark] + public float ParseRawFloat() + { + const int encodedSize = sizeof(float); + CodedInputStream cis = new CodedInputStream(floatInputBuffer); + float sum = 0; + for (int i = 0; i < BytesToParse / encodedSize; i++) + { + sum += cis.ReadFloat(); + } + return sum; + } + + [Benchmark] + public double ParseRawDouble() + { + const int encodedSize = sizeof(double); + CodedInputStream cis = new CodedInputStream(doubleInputBuffer); + double sum = 0; + for (int i = 0; i < BytesToParse / encodedSize; i++) + { + sum += cis.ReadDouble(); + } + return sum; + } + + private static byte[] CreateBufferWithRandomVarints(Random random, int valueCount, int encodedSize, int paddingValueCount) + { + MemoryStream ms = new MemoryStream(); + CodedOutputStream cos = new CodedOutputStream(ms); + for (int i = 0; i < valueCount + paddingValueCount; i++) + { + cos.WriteUInt64(RandomUnsignedVarint(random, encodedSize)); + } + cos.Flush(); + var buffer = ms.ToArray(); + + if (buffer.Length != encodedSize * (valueCount + paddingValueCount)) + { + throw new InvalidOperationException($"Unexpected output buffer length {buffer.Length}"); + } + return buffer; + } + + private static byte[] CreateBufferWithRandomFloats(Random random, int valueCount, int paddingValueCount) + { + MemoryStream ms = new MemoryStream(); + CodedOutputStream cos = new CodedOutputStream(ms); + for (int i = 0; i < valueCount + paddingValueCount; i++) + { + cos.WriteFloat((float)random.NextDouble()); + } + cos.Flush(); + var buffer = ms.ToArray(); + return buffer; + } + + private static byte[] CreateBufferWithRandomDoubles(Random random, int valueCount, int paddingValueCount) + { + MemoryStream ms = new MemoryStream(); + CodedOutputStream cos = new CodedOutputStream(ms); + for (int i = 0; i < valueCount + paddingValueCount; i++) + { + cos.WriteDouble(random.NextDouble()); + } + cos.Flush(); + var buffer = ms.ToArray(); + return buffer; + } + + private static byte[] CreateBufferWithRandomData(Random random, int valueCount, int encodedSize, int paddingValueCount) + { + int bufferSize = (valueCount + paddingValueCount) * encodedSize; + byte[] buffer = new byte[bufferSize]; + random.NextBytes(buffer); + return buffer; + } + + /// + /// Generate a random value that will take exactly "encodedSize" bytes when varint-encoded. + /// + private static ulong RandomUnsignedVarint(Random random, int encodedSize) + { + Span randomBytesBuffer = stackalloc byte[8]; + + if (encodedSize < 1 || encodedSize > 10) + { + throw new ArgumentException("Illegal encodedSize value requested", nameof(encodedSize)); + } + const int bitsPerByte = 7; + + ulong result = 0; + while (true) + { + random.NextBytes(randomBytesBuffer); + ulong randomValue = BinaryPrimitives.ReadUInt64LittleEndian(randomBytesBuffer); + + // only use the number of random bits we need + ulong bitmask = encodedSize < 10 ? ((1UL << (encodedSize * bitsPerByte)) - 1) : ulong.MaxValue; + result = randomValue & bitmask; + + if (encodedSize == 10) + { + // for 10-byte values the highest bit always needs to be set (7*9=63) + result |= ulong.MaxValue; + break; + } + + // some random values won't require the full "encodedSize" bytes, check that at least + // one of the top 7 bits is set. Retrying is fine since it only happens rarely + if (encodedSize == 1 || (result & (0x7FUL << ((encodedSize - 1) * bitsPerByte))) != 0) + { + break; + } + } + return result; + } + } +}