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
33using BenchmarkDotNet.Attributes;
34using System;
35using System.Buffers.Binary;
36using System.Collections.Generic;
37using System.IO;
38using System.Buffers;
39using System.Text;
40
41namespace Google.Protobuf.Benchmarks
42{
43    /// <summary>
44    /// Benchmarks throughput when writing raw primitives.
45    /// </summary>
46    [MemoryDiagnoser]
47    public class WriteRawPrimitivesBenchmark
48    {
49        // key is the encodedSize of varint values
50        Dictionary<int, uint[]> varint32Values;
51        Dictionary<int, ulong[]> varint64Values;
52
53        double[] doubleValues;
54        float[] floatValues;
55
56        // key is the encodedSize of string values
57        Dictionary<int, string[]> stringValues;
58
59        // key is the encodedSize of string values
60        Dictionary<int, string[]> nonAsciiStringValues;
61
62        // key is the encodedSize of string values
63        Dictionary<int, ByteString[]> byteStringValues;
64
65        // the buffer to which all the data will be written
66        byte[] outputBuffer;
67
68        Random random = new Random(417384220);  // random but deterministic seed
69
70        public IEnumerable<int> StringEncodedSizes => new[] { 1, 4, 10, 105, 10080 };
71
72        public IEnumerable<int> NonAsciiStringEncodedSizes => new[] { 4, 10, 105, 10080 };
73
74        [GlobalSetup]
75        public void GlobalSetup()
76        {
77            outputBuffer = new byte[BytesToWrite];
78
79            varint32Values = new Dictionary<int, uint[]>();
80            varint64Values = new Dictionary<int, ulong[]>();
81            for (int encodedSize = 1; encodedSize <= 10; encodedSize++)
82            {
83                if (encodedSize <= 5)
84                {
85                    varint32Values.Add(encodedSize, CreateRandomVarints32(random, BytesToWrite / encodedSize, encodedSize));
86                }
87                varint64Values.Add(encodedSize, CreateRandomVarints64(random, BytesToWrite / encodedSize, encodedSize));
88            }
89
90            doubleValues = CreateRandomDoubles(random, BytesToWrite / sizeof(double));
91            floatValues = CreateRandomFloats(random, BytesToWrite / sizeof(float));
92
93            stringValues = new Dictionary<int, string[]>();
94
95            byteStringValues = new Dictionary<int, ByteString[]>();
96            foreach(var encodedSize in StringEncodedSizes)
97            {
98                stringValues.Add(encodedSize, CreateStrings(BytesToWrite / encodedSize, encodedSize));
99                byteStringValues.Add(encodedSize, CreateByteStrings(BytesToWrite / encodedSize, encodedSize));
100            }
101
102            nonAsciiStringValues = new Dictionary<int, string[]>();
103            foreach(var encodedSize in NonAsciiStringEncodedSizes)
104            {
105                nonAsciiStringValues.Add(encodedSize, CreateNonAsciiStrings(BytesToWrite / encodedSize, encodedSize));
106            }
107        }
108
109        // Total number of bytes that each benchmark will write.
110        // Measuring the time taken to write buffer of given size makes it easier to compare parsing speed for different
111        // types and makes it easy to calculate the througput (in MB/s)
112        // 10800 bytes is chosen because it is divisible by all possible encoded sizes for all primitive types {1..10}
113        [Params(10080)]
114        public int BytesToWrite { get; set; }
115
116        [Benchmark]
117        [Arguments(1)]
118        [Arguments(2)]
119        [Arguments(3)]
120        [Arguments(4)]
121        [Arguments(5)]
122        public void WriteRawVarint32_CodedOutputStream(int encodedSize)
123        {
124            var values = varint32Values[encodedSize];
125            var cos = new CodedOutputStream(outputBuffer);
126            for (int i = 0; i < values.Length; i++)
127            {
128                cos.WriteRawVarint32(values[i]);
129            }
130            cos.Flush();
131            cos.CheckNoSpaceLeft();
132        }
133
134        [Benchmark]
135        [Arguments(1)]
136        [Arguments(2)]
137        [Arguments(3)]
138        [Arguments(4)]
139        [Arguments(5)]
140        public void WriteRawVarint32_WriteContext(int encodedSize)
141        {
142            var values = varint32Values[encodedSize];
143            var span = new Span<byte>(outputBuffer);
144            WriteContext.Initialize(ref span, out WriteContext ctx);
145            for (int i = 0; i < values.Length; i++)
146            {
147                ctx.WriteUInt32(values[i]);
148            }
149            ctx.Flush();
150            ctx.CheckNoSpaceLeft();
151        }
152
153        [Benchmark]
154        [Arguments(1)]
155        [Arguments(2)]
156        [Arguments(3)]
157        [Arguments(4)]
158        [Arguments(5)]
159        [Arguments(6)]
160        [Arguments(7)]
161        [Arguments(8)]
162        [Arguments(9)]
163        [Arguments(10)]
164        public void WriteRawVarint64_CodedOutputStream(int encodedSize)
165        {
166            var values = varint64Values[encodedSize];
167            var cos = new CodedOutputStream(outputBuffer);
168            for (int i = 0; i < values.Length; i++)
169            {
170                cos.WriteRawVarint64(values[i]);
171            }
172            cos.Flush();
173            cos.CheckNoSpaceLeft();
174        }
175
176        [Benchmark]
177        [Arguments(1)]
178        [Arguments(2)]
179        [Arguments(3)]
180        [Arguments(4)]
181        [Arguments(5)]
182        [Arguments(6)]
183        [Arguments(7)]
184        [Arguments(8)]
185        [Arguments(9)]
186        [Arguments(10)]
187        public void WriteRawVarint64_WriteContext(int encodedSize)
188        {
189            var values = varint64Values[encodedSize];
190            var span = new Span<byte>(outputBuffer);
191            WriteContext.Initialize(ref span, out WriteContext ctx);
192            for (int i = 0; i < values.Length; i++)
193            {
194                ctx.WriteUInt64(values[i]);
195            }
196            ctx.Flush();
197            ctx.CheckNoSpaceLeft();
198        }
199
200        [Benchmark]
201        public void WriteFixed32_CodedOutputStream()
202        {
203            const int encodedSize = sizeof(uint);
204            var cos = new CodedOutputStream(outputBuffer);
205            for (int i = 0; i < BytesToWrite / encodedSize; i++)
206            {
207                cos.WriteFixed32(12345);
208            }
209            cos.Flush();
210            cos.CheckNoSpaceLeft();
211        }
212
213        [Benchmark]
214        public void WriteFixed32_WriteContext()
215        {
216            const int encodedSize = sizeof(uint);
217            var span = new Span<byte>(outputBuffer);
218            WriteContext.Initialize(ref span, out WriteContext ctx);
219            for (uint i = 0; i < BytesToWrite / encodedSize; i++)
220            {
221                ctx.WriteFixed32(12345);
222            }
223            ctx.Flush();
224            ctx.CheckNoSpaceLeft();
225        }
226
227        [Benchmark]
228        public void WriteFixed64_CodedOutputStream()
229        {
230            const int encodedSize = sizeof(ulong);
231            var cos = new CodedOutputStream(outputBuffer);
232            for(int i = 0; i < BytesToWrite / encodedSize; i++)
233            {
234                cos.WriteFixed64(123456789);
235            }
236            cos.Flush();
237            cos.CheckNoSpaceLeft();
238        }
239
240        [Benchmark]
241        public void WriteFixed64_WriteContext()
242        {
243            const int encodedSize = sizeof(ulong);
244            var span = new Span<byte>(outputBuffer);
245            WriteContext.Initialize(ref span, out WriteContext ctx);
246            for (uint i = 0; i < BytesToWrite / encodedSize; i++)
247            {
248                ctx.WriteFixed64(123456789);
249            }
250            ctx.Flush();
251            ctx.CheckNoSpaceLeft();
252        }
253
254        [Benchmark]
255        public void WriteRawTag_OneByte_WriteContext()
256        {
257            const int encodedSize = 1;
258            var span = new Span<byte>(outputBuffer);
259            WriteContext.Initialize(ref span, out WriteContext ctx);
260            for (uint i = 0; i < BytesToWrite / encodedSize; i++)
261            {
262                ctx.WriteRawTag(16);
263            }
264            ctx.Flush();
265            ctx.CheckNoSpaceLeft();
266        }
267
268        [Benchmark]
269        public void WriteRawTag_TwoBytes_WriteContext()
270        {
271            const int encodedSize = 2;
272            var span = new Span<byte>(outputBuffer);
273            WriteContext.Initialize(ref span, out WriteContext ctx);
274            for (uint i = 0; i < BytesToWrite / encodedSize; i++)
275            {
276                ctx.WriteRawTag(137, 6);
277            }
278            ctx.Flush();
279            ctx.CheckNoSpaceLeft();
280        }
281
282        [Benchmark]
283        public void WriteRawTag_ThreeBytes_WriteContext()
284        {
285            const int encodedSize = 3;
286            var span = new Span<byte>(outputBuffer);
287            WriteContext.Initialize(ref span, out WriteContext ctx);
288            for (uint i = 0; i < BytesToWrite / encodedSize; i++)
289            {
290                ctx.WriteRawTag(160, 131, 1);
291            }
292            ctx.Flush();
293            ctx.CheckNoSpaceLeft();
294        }
295
296        [Benchmark]
297        public void Baseline_WriteContext()
298        {
299            var span = new Span<byte>(outputBuffer);
300            WriteContext.Initialize(ref span, out WriteContext ctx);
301            ctx.state.position = outputBuffer.Length;
302            ctx.Flush();
303            ctx.CheckNoSpaceLeft();
304        }
305
306        [Benchmark]
307        public void WriteRawFloat_CodedOutputStream()
308        {
309            var cos = new CodedOutputStream(outputBuffer);
310            foreach (var value in floatValues)
311            {
312                cos.WriteFloat(value);
313            }
314            cos.Flush();
315            cos.CheckNoSpaceLeft();
316        }
317
318        [Benchmark]
319        public void WriteRawFloat_WriteContext()
320        {
321            var span = new Span<byte>(outputBuffer);
322            WriteContext.Initialize(ref span, out WriteContext ctx);
323            foreach (var value in floatValues)
324            {
325                ctx.WriteFloat(value);
326            }
327            ctx.Flush();
328            ctx.CheckNoSpaceLeft();
329        }
330
331        [Benchmark]
332        public void WriteRawDouble_CodedOutputStream()
333        {
334            var cos = new CodedOutputStream(outputBuffer);
335            foreach (var value in doubleValues)
336            {
337                cos.WriteDouble(value);
338            }
339            cos.Flush();
340            cos.CheckNoSpaceLeft();
341        }
342
343        [Benchmark]
344        public void WriteRawDouble_WriteContext()
345        {
346            var span = new Span<byte>(outputBuffer);
347            WriteContext.Initialize(ref span, out WriteContext ctx);
348            foreach (var value in doubleValues)
349            {
350                ctx.WriteDouble(value);
351            }
352            ctx.Flush();
353            ctx.CheckNoSpaceLeft();
354        }
355
356        [Benchmark]
357        [ArgumentsSource(nameof(StringEncodedSizes))]
358        public void WriteString_CodedOutputStream(int encodedSize)
359        {
360            var values = stringValues[encodedSize];
361            var cos = new CodedOutputStream(outputBuffer);
362            foreach (var value in values)
363            {
364                cos.WriteString(value);
365            }
366            cos.Flush();
367            cos.CheckNoSpaceLeft();
368        }
369
370        [Benchmark]
371        [ArgumentsSource(nameof(StringEncodedSizes))]
372        public void WriteString_WriteContext(int encodedSize)
373        {
374            var values = stringValues[encodedSize];
375            var span = new Span<byte>(outputBuffer);
376            WriteContext.Initialize(ref span, out WriteContext ctx);
377            foreach (var value in values)
378            {
379                ctx.WriteString(value);
380            }
381            ctx.Flush();
382            ctx.CheckNoSpaceLeft();
383        }
384
385        [Benchmark]
386        [ArgumentsSource(nameof(NonAsciiStringEncodedSizes))]
387        public void WriteNonAsciiString_CodedOutputStream(int encodedSize)
388        {
389            var values = nonAsciiStringValues[encodedSize];
390            var cos = new CodedOutputStream(outputBuffer);
391            foreach (var value in values)
392            {
393                cos.WriteString(value);
394            }
395            cos.Flush();
396            cos.CheckNoSpaceLeft();
397        }
398
399        [Benchmark]
400        [ArgumentsSource(nameof(NonAsciiStringEncodedSizes))]
401        public void WriteNonAsciiString_WriteContext(int encodedSize)
402        {
403            var values = nonAsciiStringValues[encodedSize];
404            var span = new Span<byte>(outputBuffer);
405            WriteContext.Initialize(ref span, out WriteContext ctx);
406            foreach (var value in values)
407            {
408                ctx.WriteString(value);
409            }
410            ctx.Flush();
411            ctx.CheckNoSpaceLeft();
412        }
413
414        [Benchmark]
415        [ArgumentsSource(nameof(StringEncodedSizes))]
416        public void WriteBytes_CodedOutputStream(int encodedSize)
417        {
418            var values = byteStringValues[encodedSize];
419            var cos = new CodedOutputStream(outputBuffer);
420            foreach (var value in values)
421            {
422                cos.WriteBytes(value);
423            }
424            cos.Flush();
425            cos.CheckNoSpaceLeft();
426        }
427
428        [Benchmark]
429        [ArgumentsSource(nameof(StringEncodedSizes))]
430        public void WriteBytes_WriteContext(int encodedSize)
431        {
432            var values = byteStringValues[encodedSize];
433            var span = new Span<byte>(outputBuffer);
434            WriteContext.Initialize(ref span, out WriteContext ctx);
435            foreach (var value in values)
436            {
437                ctx.WriteBytes(value);
438            }
439            ctx.Flush();
440            ctx.CheckNoSpaceLeft();
441        }
442
443        private static uint[] CreateRandomVarints32(Random random, int valueCount, int encodedSize)
444        {
445            var result = new uint[valueCount];
446            for (int i = 0; i < valueCount; i++)
447            {
448                result[i] = (uint) ParseRawPrimitivesBenchmark.RandomUnsignedVarint(random, encodedSize, true);
449            }
450            return result;
451        }
452
453        private static ulong[] CreateRandomVarints64(Random random, int valueCount, int encodedSize)
454        {
455            var result = new ulong[valueCount];
456            for (int i = 0; i < valueCount; i++)
457            {
458                result[i] = ParseRawPrimitivesBenchmark.RandomUnsignedVarint(random, encodedSize, false);
459            }
460            return result;
461        }
462
463        private static float[] CreateRandomFloats(Random random, int valueCount)
464        {
465            var result = new float[valueCount];
466            for (int i = 0; i < valueCount; i++)
467            {
468                result[i] = (float)random.NextDouble();
469            }
470            return result;
471        }
472
473        private static double[] CreateRandomDoubles(Random random, int valueCount)
474        {
475            var result = new double[valueCount];
476            for (int i = 0; i < valueCount; i++)
477            {
478                result[i] = random.NextDouble();
479            }
480            return result;
481        }
482
483        private static string[] CreateStrings(int valueCount, int encodedSize)
484        {
485            var str = ParseRawPrimitivesBenchmark.CreateStringWithEncodedSize(encodedSize);
486
487            var result = new string[valueCount];
488            for (int i = 0; i < valueCount; i++)
489            {
490                result[i] = str;
491            }
492            return result;
493        }
494
495        private static string[] CreateNonAsciiStrings(int valueCount, int encodedSize)
496        {
497            var str = ParseRawPrimitivesBenchmark.CreateNonAsciiStringWithEncodedSize(encodedSize);
498
499            var result = new string[valueCount];
500            for (int i = 0; i < valueCount; i++)
501            {
502                result[i] = str;
503            }
504            return result;
505        }
506
507        private static ByteString[] CreateByteStrings(int valueCount, int encodedSize)
508        {
509            var str = ParseRawPrimitivesBenchmark.CreateStringWithEncodedSize(encodedSize);
510
511            var result = new ByteString[valueCount];
512            for (int i = 0; i < valueCount; i++)
513            {
514                result[i] = ByteString.CopyFrom(Encoding.UTF8.GetBytes(str));
515            }
516            return result;
517        }
518    }
519}
520