diff --git a/csharp/src/Google.Protobuf.Benchmarks/ParseRawPrimitivesBenchmark.cs b/csharp/src/Google.Protobuf.Benchmarks/ParseRawPrimitivesBenchmark.cs index 863e74dc49..75658a9ef8 100644 --- a/csharp/src/Google.Protobuf.Benchmarks/ParseRawPrimitivesBenchmark.cs +++ b/csharp/src/Google.Protobuf.Benchmarks/ParseRawPrimitivesBenchmark.cs @@ -337,7 +337,7 @@ namespace Google.Protobuf.Benchmarks CodedOutputStream cos = new CodedOutputStream(ms); for (int i = 0; i < valueCount + paddingValueCount; i++) { - cos.WriteUInt64(RandomUnsignedVarint(random, encodedSize)); + cos.WriteUInt64(RandomUnsignedVarint(random, encodedSize, false)); } cos.Flush(); var buffer = ms.ToArray(); @@ -386,11 +386,11 @@ namespace Google.Protobuf.Benchmarks /// /// Generate a random value that will take exactly "encodedSize" bytes when varint-encoded. /// - private static ulong RandomUnsignedVarint(Random random, int encodedSize) + public static ulong RandomUnsignedVarint(Random random, int encodedSize, bool fitsIn32Bits) { Span randomBytesBuffer = stackalloc byte[8]; - if (encodedSize < 1 || encodedSize > 10) + if (encodedSize < 1 || encodedSize > 10 || (fitsIn32Bits && encodedSize > 5)) { throw new ArgumentException("Illegal encodedSize value requested", nameof(encodedSize)); } @@ -406,6 +406,12 @@ namespace Google.Protobuf.Benchmarks ulong bitmask = encodedSize < 10 ? ((1UL << (encodedSize * bitsPerByte)) - 1) : ulong.MaxValue; result = randomValue & bitmask; + if (fitsIn32Bits) + { + // make sure the resulting value is representable by a uint. + result &= uint.MaxValue; + } + if (encodedSize == 10) { // for 10-byte values the highest bit always needs to be set (7*9=63) @@ -443,7 +449,7 @@ namespace Google.Protobuf.Benchmarks return buffer; } - private static string CreateStringWithEncodedSize(int encodedSize) + public static string CreateStringWithEncodedSize(int encodedSize) { var str = new string('a', encodedSize); while (CodedOutputStream.ComputeStringSize(str) > encodedSize) diff --git a/csharp/src/Google.Protobuf.Benchmarks/WriteRawPrimitivesBenchmark.cs b/csharp/src/Google.Protobuf.Benchmarks/WriteRawPrimitivesBenchmark.cs new file mode 100644 index 0000000000..7dacc9c48c --- /dev/null +++ b/csharp/src/Google.Protobuf.Benchmarks/WriteRawPrimitivesBenchmark.cs @@ -0,0 +1,414 @@ +#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; +using System.Buffers; +using System.Text; + +namespace Google.Protobuf.Benchmarks +{ + /// + /// Benchmarks throughput when writing raw primitives. + /// + [MemoryDiagnoser] + public class WriteRawPrimitivesBenchmark + { + // key is the encodedSize of varint values + Dictionary varint32Values; + Dictionary varint64Values; + + double[] doubleValues; + float[] floatValues; + + // key is the encodedSize of string values + Dictionary stringValues; + + // key is the encodedSize of string values + Dictionary byteStringValues; + + // the buffer to which all the data will be written + byte[] outputBuffer; + + Random random = new Random(417384220); // random but deterministic seed + + public IEnumerable StringEncodedSizes => new[] { 1, 4, 10, 105, 10080 }; + + [GlobalSetup] + public void GlobalSetup() + { + outputBuffer = new byte[BytesToWrite]; + + varint32Values = new Dictionary(); + varint64Values = new Dictionary(); + for (int encodedSize = 1; encodedSize <= 10; encodedSize++) + { + if (encodedSize <= 5) + { + varint32Values.Add(encodedSize, CreateRandomVarints32(random, BytesToWrite / encodedSize, encodedSize)); + } + varint64Values.Add(encodedSize, CreateRandomVarints64(random, BytesToWrite / encodedSize, encodedSize)); + } + + doubleValues = CreateRandomDoubles(random, BytesToWrite / sizeof(double)); + floatValues = CreateRandomFloats(random, BytesToWrite / sizeof(float)); + + stringValues = new Dictionary(); + byteStringValues = new Dictionary(); + foreach(var encodedSize in StringEncodedSizes) + { + stringValues.Add(encodedSize, CreateStrings(BytesToWrite / encodedSize, encodedSize)); + byteStringValues.Add(encodedSize, CreateByteStrings(BytesToWrite / encodedSize, encodedSize)); + } + } + + // Total number of bytes that each benchmark will write. + // Measuring the time taken to write 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 BytesToWrite { get; set; } + + [Benchmark] + [Arguments(1)] + [Arguments(2)] + [Arguments(3)] + [Arguments(4)] + [Arguments(5)] + public void WriteRawVarint32_CodedOutputStream(int encodedSize) + { + var values = varint32Values[encodedSize]; + var cos = new CodedOutputStream(outputBuffer); + foreach (var value in values) + { + cos.WriteRawVarint32(value); + } + cos.Flush(); + cos.CheckNoSpaceLeft(); + } + + [Benchmark] + [Arguments(1)] + [Arguments(2)] + [Arguments(3)] + [Arguments(4)] + [Arguments(5)] + public void WriteRawVarint32_WriteContext(int encodedSize) + { + var values = varint32Values[encodedSize]; + var span = new Span(outputBuffer); + WriteContext.Initialize(ref span, out WriteContext ctx); + foreach (var value in values) + { + ctx.WriteUInt32(value); + } + ctx.Flush(); + ctx.CheckNoSpaceLeft(); + } + + [Benchmark] + [Arguments(1)] + [Arguments(2)] + [Arguments(3)] + [Arguments(4)] + [Arguments(5)] + [Arguments(6)] + [Arguments(7)] + [Arguments(8)] + [Arguments(9)] + [Arguments(10)] + public void WriteRawVarint64_CodedOutputStream(int encodedSize) + { + var values = varint64Values[encodedSize]; + var cos = new CodedOutputStream(outputBuffer); + foreach (var value in values) + { + cos.WriteRawVarint64(value); + } + cos.Flush(); + cos.CheckNoSpaceLeft(); + } + + [Benchmark] + [Arguments(1)] + [Arguments(2)] + [Arguments(3)] + [Arguments(4)] + [Arguments(5)] + [Arguments(6)] + [Arguments(7)] + [Arguments(8)] + [Arguments(9)] + [Arguments(10)] + public void WriteRawVarint64_WriteContext(int encodedSize) + { + var values = varint64Values[encodedSize]; + var span = new Span(outputBuffer); + WriteContext.Initialize(ref span, out WriteContext ctx); + foreach (var value in values) + { + ctx.WriteUInt64(value); + } + ctx.Flush(); + ctx.CheckNoSpaceLeft(); + } + + [Benchmark] + public void WriteFixed32_CodedOutputStream() + { + const int encodedSize = sizeof(uint); + var cos = new CodedOutputStream(outputBuffer); + for(int i = 0; i < BytesToWrite / encodedSize; i++) + { + cos.WriteFixed32(12345); + } + cos.Flush(); + cos.CheckNoSpaceLeft(); + } + + [Benchmark] + public void WriteFixed32_WriteContext() + { + const int encodedSize = sizeof(uint); + var span = new Span(outputBuffer); + WriteContext.Initialize(ref span, out WriteContext ctx); + for (uint i = 0; i < BytesToWrite / encodedSize; i++) + { + ctx.WriteFixed32(12345); + } + ctx.Flush(); + ctx.CheckNoSpaceLeft(); + } + + [Benchmark] + public void WriteFixed64_CodedOutputStream() + { + const int encodedSize = sizeof(ulong); + var cos = new CodedOutputStream(outputBuffer); + for(int i = 0; i < BytesToWrite / encodedSize; i++) + { + cos.WriteFixed64(123456789); + } + cos.Flush(); + cos.CheckNoSpaceLeft(); + } + + [Benchmark] + public void WriteFixed64_WriteContext() + { + const int encodedSize = sizeof(uint); + var span = new Span(outputBuffer); + WriteContext.Initialize(ref span, out WriteContext ctx); + for (uint i = 0; i < BytesToWrite / encodedSize; i++) + { + ctx.WriteFixed64(123456789); + } + ctx.Flush(); + ctx.CheckNoSpaceLeft(); + } + + [Benchmark] + public void WriteRawFloat_CodedOutputStream() + { + var cos = new CodedOutputStream(outputBuffer); + foreach (var value in floatValues) + { + cos.WriteFloat(value); + } + cos.Flush(); + cos.CheckNoSpaceLeft(); + } + + [Benchmark] + public void WriteRawFloat_WriteContext() + { + var span = new Span(outputBuffer); + WriteContext.Initialize(ref span, out WriteContext ctx); + foreach (var value in floatValues) + { + ctx.WriteFloat(value); + } + ctx.Flush(); + ctx.CheckNoSpaceLeft(); + } + + [Benchmark] + public void WriteRawDouble_CodedOutputStream() + { + var cos = new CodedOutputStream(outputBuffer); + foreach (var value in doubleValues) + { + cos.WriteDouble(value); + } + cos.Flush(); + cos.CheckNoSpaceLeft(); + } + + [Benchmark] + public void WriteRawDouble_WriteContext() + { + var span = new Span(outputBuffer); + WriteContext.Initialize(ref span, out WriteContext ctx); + foreach (var value in doubleValues) + { + ctx.WriteDouble(value); + } + ctx.Flush(); + ctx.CheckNoSpaceLeft(); + } + + [Benchmark] + [ArgumentsSource(nameof(StringEncodedSizes))] + public void WriteString_CodedOutputStream(int encodedSize) + { + var values = stringValues[encodedSize]; + var cos = new CodedOutputStream(outputBuffer); + foreach (var value in values) + { + cos.WriteString(value); + } + cos.Flush(); + cos.CheckNoSpaceLeft(); + } + + [Benchmark] + [ArgumentsSource(nameof(StringEncodedSizes))] + public void WriteString_WriteContext(int encodedSize) + { + var values = stringValues[encodedSize]; + var span = new Span(outputBuffer); + WriteContext.Initialize(ref span, out WriteContext ctx); + foreach (var value in values) + { + ctx.WriteString(value); + } + ctx.Flush(); + ctx.CheckNoSpaceLeft(); + } + + [Benchmark] + [ArgumentsSource(nameof(StringEncodedSizes))] + public void WriteBytes_CodedOutputStream(int encodedSize) + { + var values = byteStringValues[encodedSize]; + var cos = new CodedOutputStream(outputBuffer); + foreach (var value in values) + { + cos.WriteBytes(value); + } + cos.Flush(); + cos.CheckNoSpaceLeft(); + } + + [Benchmark] + [ArgumentsSource(nameof(StringEncodedSizes))] + public void WriteBytes_WriteContext(int encodedSize) + { + var values = byteStringValues[encodedSize]; + var span = new Span(outputBuffer); + WriteContext.Initialize(ref span, out WriteContext ctx); + foreach (var value in values) + { + ctx.WriteBytes(value); + } + ctx.Flush(); + ctx.CheckNoSpaceLeft(); + } + + private static uint[] CreateRandomVarints32(Random random, int valueCount, int encodedSize) + { + var result = new uint[valueCount]; + for (int i = 0; i < valueCount; i++) + { + result[i] = (uint) ParseRawPrimitivesBenchmark.RandomUnsignedVarint(random, encodedSize, true); + } + return result; + } + + private static ulong[] CreateRandomVarints64(Random random, int valueCount, int encodedSize) + { + var result = new ulong[valueCount]; + for (int i = 0; i < valueCount; i++) + { + result[i] = ParseRawPrimitivesBenchmark.RandomUnsignedVarint(random, encodedSize, false); + } + return result; + } + + private static float[] CreateRandomFloats(Random random, int valueCount) + { + var result = new float[valueCount]; + for (int i = 0; i < valueCount; i++) + { + result[i] = (float)random.NextDouble(); + } + return result; + } + + private static double[] CreateRandomDoubles(Random random, int valueCount) + { + var result = new double[valueCount]; + for (int i = 0; i < valueCount; i++) + { + result[i] = random.NextDouble(); + } + return result; + } + + private static string[] CreateStrings(int valueCount, int encodedSize) + { + var str = ParseRawPrimitivesBenchmark.CreateStringWithEncodedSize(encodedSize); + + var result = new string[valueCount]; + for (int i = 0; i < valueCount; i++) + { + result[i] = str; + } + return result; + } + + private static ByteString[] CreateByteStrings(int valueCount, int encodedSize) + { + var str = ParseRawPrimitivesBenchmark.CreateStringWithEncodedSize(encodedSize); + + var result = new ByteString[valueCount]; + for (int i = 0; i < valueCount; i++) + { + result[i] = ByteString.CopyFrom(Encoding.UTF8.GetBytes(str)); + } + return result; + } + } +}