Selaa lähdekoodia

Merge pull request #7279 from jtattermusch/ParseRawPrimitivesBenchmark

Add benchmark for measuring raw primitive parsing speed
Jan Tattermusch 5 vuotta sitten
vanhempi
commit
f57ae5eac3
2 muutettua tiedostoa jossa 266 lisäystä ja 0 poistoa
  1. 1 0
      Makefile.am
  2. 265 0
      csharp/src/Google.Protobuf.Benchmarks/ParseRawPrimitivesBenchmark.cs

+ 1 - 0
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               \

+ 265 - 0
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
+{
+    /// <summary>
+    /// Benchmarks throughput when parsing raw primitives.
+    /// </summary>
+    [MemoryDiagnoser]
+    public class ParseRawPrimitivesBenchmark
+    {
+        // key is the encodedSize of varint values
+        Dictionary<int, byte[]> 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<int, byte[]>();
+            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;
+        }
+
+        /// <summary>
+        /// Generate a random value that will take exactly "encodedSize" bytes when varint-encoded.
+        /// </summary>
+        private static ulong RandomUnsignedVarint(Random random, int encodedSize)
+        {
+            Span<byte> 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;
+        }
+    }
+}