From fcdd616b7de67aed188bb8dd777011a54c8bf725 Mon Sep 17 00:00:00 2001 From: mircodz Date: Fri, 2 Oct 2026 00:59:21 +0200 Subject: [PATCH 1/4] perf(runtime): faster varints Varints of one or two bytes (values below 2^14) are read and written inline, with one bounds check for both bytes. Longer ones go out of line, through pdep and pext where BMI2 is fast: everywhere but AMD families 15h to 18h. Reads still consume the same bytes and return the same values as upstream's. --- Bond.Runtime/CompactBinaryReader.cs | 125 ++++++++++++++++++-- Bond.Runtime/CompactBinaryWriter.cs | 71 +++++++++++- Bond.Runtime/FastPdep.cs | 20 ++++ Bond.Runtime/README.md | 28 ++--- benchmarks/RESULTS.md | 172 ++++++++++++++-------------- version | 2 +- 6 files changed, 303 insertions(+), 115 deletions(-) create mode 100644 Bond.Runtime/FastPdep.cs diff --git a/Bond.Runtime/CompactBinaryReader.cs b/Bond.Runtime/CompactBinaryReader.cs index 82e2e07..e0427f8 100644 --- a/Bond.Runtime/CompactBinaryReader.cs +++ b/Bond.Runtime/CompactBinaryReader.cs @@ -2,7 +2,10 @@ using System.Buffers.Binary; using System.Diagnostics.CodeAnalysis; using System.IO; +using System.Numerics; using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Runtime.Intrinsics.X86; using System.Text; namespace BondTools.Runtime; @@ -347,32 +350,132 @@ private int ReadCount(int elementBytes) return (int)count; } - // Most varints are one byte: those are read inline. + // Varints of one or two bytes (values below 2^14) are read inline, longer ones out of line. Written out once per + // width: shared helpers measured slower. [MethodImpl(MethodImplOptions.AggressiveInlining)] - private ushort ReadVarUInt16() => TryReadOneByte(out var value) ? value : (ushort)ReadVarUInt(lastByte: 2); + private ushort ReadVarUInt16() + { + var position = _position; + var data = _data; + if ((uint)position + 1 < (uint)data.Length) + { + ref var p = ref Unsafe.Add(ref MemoryMarshal.GetReference(data), position); + uint value = p; + if (value < 0x80) + { + _position = position + 1; + return (ushort)value; + } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private uint ReadVarUInt32() => TryReadOneByte(out var value) ? value : (uint)ReadVarUInt(lastByte: 4); + uint next = Unsafe.Add(ref p, 1); + if (next < 0x80) + { + _position = position + 2; + return (ushort)((value & 0x7F) | (next << 7)); + } + } + + return (ushort)ReadVarUInt(lastByte: 2); + } [MethodImpl(MethodImplOptions.AggressiveInlining)] - private ulong ReadVarUInt64() => TryReadOneByte(out var value) ? value : ReadVarUInt(lastByte: 8); + private uint ReadVarUInt32() + { + var position = _position; + var data = _data; + if ((uint)position + 1 < (uint)data.Length) + { + ref var p = ref Unsafe.Add(ref MemoryMarshal.GetReference(data), position); + uint value = p; + if (value < 0x80) + { + _position = position + 1; + return value; + } + + uint next = Unsafe.Add(ref p, 1); + if (next < 0x80) + { + _position = position + 2; + return (value & 0x7F) | (next << 7); + } + } + + return ReadVarUInt32Slow(); + } [MethodImpl(MethodImplOptions.AggressiveInlining)] - private bool TryReadOneByte(out byte value) + private ulong ReadVarUInt64() { - if ((uint)_position < (uint)_data.Length && _data[_position] < 0x80) + var position = _position; + var data = _data; + if ((uint)position + 1 < (uint)data.Length) + { + ref var p = ref Unsafe.Add(ref MemoryMarshal.GetReference(data), position); + ulong value = p; + if (value < 0x80) + { + _position = position + 1; + return value; + } + + ulong next = Unsafe.Add(ref p, 1); + if (next < 0x80) + { + _position = position + 2; + return (value & 0x7F) | (next << 7); + } + } + + return ReadVarUInt64Slow(); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private uint ReadVarUInt32Slow() + { + var position = _position; + if (FastPdep.IsSupported && _data.Length - position >= 8) + { + // Ends at the first byte that does not continue, or at the fifth, whose high bits fall off the uint. + var bytes = Unsafe.ReadUnaligned(ref Unsafe.Add(ref MemoryMarshal.GetReference(_data), position)); + var length = (BitOperations.TrailingZeroCount((~bytes & 0x80_8080_8080UL) | 0x80_0000_0000UL) + 1) >> 3; + _position = position + length; + return (uint)Bmi2.X64.ParallelBitExtract(Bmi2.X64.ZeroHighBits(bytes, (ulong)length * 8), 0x7F_7F7F_7F7FUL); + } + + return (uint)ReadVarUInt(lastByte: 4); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private ulong ReadVarUInt64Slow() + { + var position = _position; + if (FastPdep.IsSupported && _data.Length - position >= 10) { - value = _data[_position++]; - return true; + const ulong Payload = 0x7F7F_7F7F_7F7F_7F7FUL; + ref var p = ref Unsafe.Add(ref MemoryMarshal.GetReference(_data), position); + var bytes = Unsafe.ReadUnaligned(ref p); + var ends = ~bytes & 0x8080_8080_8080_8080UL; + if (ends != 0) + { + var length = (BitOperations.TrailingZeroCount(ends) + 1) >> 3; + _position = position + length; + return Bmi2.X64.ParallelBitExtract(Bmi2.X64.ZeroHighBits(bytes, (ulong)length * 8), Payload); + } + + // Eight bytes continue: the ninth is taken whole, and a tenth skipped if the ninth continues too. + ulong ninth = Unsafe.Add(ref p, 8); + _position = position + (ninth >= 0x80 ? 10 : 9); + return Bmi2.X64.ParallelBitExtract(bytes, Payload) | (ninth << 56); } - value = 0; - return false; + return ReadVarUInt(lastByte: 8); } // Where the longest varint of the type fits in the remaining data: at most lastByte + 1 bytes, the last taken // whole (and for 64 bits, a tenth skipped if the ninth continues), never rejected as too long. Near the end of the // data: up to ten bytes, each checked against the end. + [MethodImpl(MethodImplOptions.NoInlining)] private ulong ReadVarUInt(int lastByte) { var longest = lastByte == 8 ? 10 : lastByte + 1; diff --git a/Bond.Runtime/CompactBinaryWriter.cs b/Bond.Runtime/CompactBinaryWriter.cs index 17df26d..2c24449 100644 --- a/Bond.Runtime/CompactBinaryWriter.cs +++ b/Bond.Runtime/CompactBinaryWriter.cs @@ -1,6 +1,10 @@ using System; using System.Buffers; using System.Buffers.Binary; +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Runtime.Intrinsics.X86; using System.Text; namespace BondTools.Runtime; @@ -135,20 +139,81 @@ private void WriteByte(byte value) _position++; } + // Values below 2^14 (one or two bytes) are written inline, longer ones out of line. + [MethodImpl(MethodImplOptions.AggressiveInlining)] private void WriteVarUInt64(ulong value) { - var destination = Reserve(MaxVarIntSize); + var position = _position; + var buffer = _buffer; + if ((uint)position + 1 < (uint)buffer.Length) + { + ref var destination = ref Unsafe.Add(ref MemoryMarshal.GetReference(buffer), position); + if (value < 0x80) + { + destination = (byte)value; + _position = position + 1; + return; + } + + if (value < 0x4000) + { + destination = (byte)(value | 0x80); + Unsafe.Add(ref destination, 1) = (byte)(value >> 7); + _position = position + 2; + return; + } + } + + WriteVarUInt64Slow(value); + } + + // Also reached for short values when the buffer is nearly full. + [MethodImpl(MethodImplOptions.NoInlining)] + private void WriteVarUInt64Slow(ulong value) + { + ref var destination = ref MemoryMarshal.GetReference(Reserve(MaxVarIntSize)); + if (FastPdep.IsSupported && value >= 0x4000) + { + _position += Deposit(ref destination, value); + return; + } + var size = 0; while (value >= 0x80) { - destination[size++] = (byte)(value | 0x80); + Unsafe.Add(ref destination, size++) = (byte)(value | 0x80); value >>= 7; } - destination[size++] = (byte)value; + Unsafe.Add(ref destination, size++) = (byte)value; _position += size; } + // Spreads the value's 7-bit groups over bytes with pdep, setting the continuation bit on all but the last. + private static int Deposit(ref byte destination, ulong value) + { + const ulong Continues = 0x8080_8080_8080_8080UL; + var size = (70 - BitOperations.LeadingZeroCount(value)) / 7; + var groups = Bmi2.X64.ParallelBitDeposit(value, 0x7F7F_7F7F_7F7F_7F7FUL); + if (size <= 8) + { + Unsafe.WriteUnaligned(ref destination, groups | Bmi2.X64.ZeroHighBits(Continues, (ulong)(size - 1) * 8)); + return size; + } + + Unsafe.WriteUnaligned(ref destination, groups | Continues); + var rest = value >> 56; + if (rest < 0x80) + { + Unsafe.Add(ref destination, 8) = (byte)rest; + return 9; + } + + Unsafe.Add(ref destination, 8) = (byte)(rest | 0x80); + Unsafe.Add(ref destination, 9) = 1; + return 10; + } + // Returns room for at least size bytes at the current position, moving to a new buffer if needed. private Span Reserve(int size) { diff --git a/Bond.Runtime/FastPdep.cs b/Bond.Runtime/FastPdep.cs new file mode 100644 index 0000000..f97a7c6 --- /dev/null +++ b/Bond.Runtime/FastPdep.cs @@ -0,0 +1,20 @@ +using System.Runtime.Intrinsics.X86; + +namespace BondTools.Runtime; + +/// Whether varints of three or more bytes are encoded and decoded with pdep and pext. +/// +/// They take a few cycles wherever BMI2 is, except on AMD families 15h to 18h (Excavator to Zen 2, and Hygon), which +/// run them in microcode at up to hundreds of cycles. +/// +internal static class FastPdep +{ + public static readonly bool IsSupported = Bmi2.X64.IsSupported && Family() is < 0x15 or >= 0x19; + + private static uint Family() + { + var signature = (uint)X86Base.CpuId(1, 0).Eax; + var family = (signature >> 8) & 0xF; + return family == 0xF ? family + ((signature >> 20) & 0xFF) : family; + } +} diff --git a/Bond.Runtime/README.md b/Bond.Runtime/README.md index d720ec3..b783936 100644 --- a/Bond.Runtime/README.md +++ b/Bond.Runtime/README.md @@ -33,22 +33,22 @@ Mean time per call on an Intel i7-12700KF with .NET 10; upstream is Bond.Runtime | Write | bond-tools | upstream | Δ | |---|--:|--:|--:| -| Order with nested structs, strings and maps | 340 ns | 598 ns | -43% | -| 100 small structs | 1.40 µs | 1.98 µs | -29% | -| 100 `int32` | 177 ns | 232 ns | -24% | -| 1,000 `double` | 1.06 µs | 1.90 µs | -44% | -| `map`, 100 entries | 1.57 µs | 2.94 µs | -47% | -| `map`, 100 entries | 258 ns | 1.10 µs | -77% | -| 4 KB order using most Bond types | 4.75 µs | 6.84 µs | -31% | +| Order with nested structs, strings and maps | 297 ns | 586 ns | -49% | +| 100 small structs | 1.40 µs | 1.95 µs | -28% | +| 100 `int32` | 99 ns | 228 ns | -56% | +| 1,000 `double` | 1.05 µs | 1.69 µs | -38% | +| `map`, 100 entries | 1.47 µs | 2.99 µs | -51% | +| `map`, 100 entries | 203 ns | 1.11 µs | -82% | +| 4 KB order using most Bond types | 4.10 µs | 6.80 µs | -40% | | Read | bond-tools | upstream | Δ | |---|--:|--:|--:| -| Order with nested structs, strings and maps | 621 ns | 792 ns | -22% | -| 100 small structs | 2.45 µs | 3.20 µs | -23% | -| 100 `int32` | 303 ns | 344 ns | -12% | -| 1,000 `double` | 1.43 µs | 3.03 µs | -53% | -| `map`, 100 entries | 2.79 µs | 3.90 µs | -28% | -| `map`, 100 entries | 599 ns | 1.74 µs | -66% | -| 4 KB order using most Bond types | 7.75 µs | 11.4 µs | -32% | +| Order with nested structs, strings and maps | 617 ns | 792 ns | -22% | +| 100 small structs | 2.26 µs | 3.34 µs | -32% | +| 100 `int32` | 182 ns | 344 ns | -47% | +| 1,000 `double` | 1.44 µs | 3.23 µs | -55% | +| `map`, 100 entries | 2.79 µs | 3.91 µs | -29% | +| `map`, 100 entries | 513 ns | 1.74 µs | -70% | +| 4 KB order using most Bond types | 7.52 µs | 11.4 µs | -34% | `dotnet run -c Release --project benchmarks` runs them. [benchmarks/RESULTS.md](../benchmarks/RESULTS.md) has the full results, also for an Apple M4 Pro. diff --git a/benchmarks/RESULTS.md b/benchmarks/RESULTS.md index f9b6ced..dd3b942 100644 --- a/benchmarks/RESULTS.md +++ b/benchmarks/RESULTS.md @@ -14,49 +14,49 @@ BenchmarkDotNet v0.15.8, Linux Arch Linux IterationCount=12 LaunchCount=1 WarmupCount=6 ``` -| Method | Categories | Mean | Error | StdDev | Ratio | RatioSD | Gen0 | Gen1 | Allocated | Alloc Ratio | -|-------------------------- |--------------------------- |------------:|---------:|---------:|------:|--------:|-------:|-------:|----------:|------------:| -| ReadDoubles | 1,000 double, read | 1,433.2 ns | 18.36 ns | 14.34 ns | 1.00 | 0.01 | 0.6180 | 0.0172 | 8080 B | 1.00 | -| ReadDoublesUpstream | 1,000 double, read | 3,028.2 ns | 18.81 ns | 14.69 ns | 2.11 | 0.02 | 0.6180 | 0.0153 | 8080 B | 1.00 | -| | | | | | | | | | | | -| WriteDoubles | 1,000 double, write | 1,057.3 ns | 8.56 ns | 6.69 ns | 1.00 | 0.01 | - | - | - | NA | -| WriteDoublesUpstream | 1,000 double, write | 1,895.5 ns | 10.98 ns | 8.58 ns | 1.79 | 0.01 | - | - | - | NA | -| | | | | | | | | | | | -| ReadInts | 100 int32, read | 303.2 ns | 3.24 ns | 2.14 ns | 1.00 | 0.01 | 0.0367 | - | 480 B | 1.00 | -| ReadIntsUpstream | 100 int32, read | 344.1 ns | 3.44 ns | 2.68 ns | 1.13 | 0.01 | 0.0367 | - | 480 B | 1.00 | -| | | | | | | | | | | | -| WriteInts | 100 int32, write | 176.6 ns | 1.54 ns | 1.20 ns | 1.00 | 0.01 | - | - | - | NA | -| WriteIntsUpstream | 100 int32, write | 231.9 ns | 2.26 ns | 1.76 ns | 1.31 | 0.01 | - | - | - | NA | -| | | | | | | | | | | | -| ReadItems | 100 structs, read | 2,451.6 ns | 36.78 ns | 26.59 ns | 1.00 | 0.01 | 0.6790 | 0.0191 | 8880 B | 1.00 | -| ReadItemsUpstream | 100 structs, read | 3,196.6 ns | 33.46 ns | 26.12 ns | 1.30 | 0.02 | 0.6790 | 0.0191 | 8880 B | 1.00 | -| | | | | | | | | | | | -| WriteItems | 100 structs, write | 1,397.3 ns | 4.11 ns | 2.97 ns | 1.00 | 0.00 | - | - | - | NA | -| WriteItemsUpstream | 100 structs, write | 1,981.4 ns | 11.74 ns | 8.49 ns | 1.42 | 0.01 | - | - | - | NA | -| | | | | | | | | | | | -| ReadBig | 4 KB order, read | 7,748.6 ns | 82.63 ns | 54.66 ns | 1.00 | 0.01 | 1.9226 | 0.1373 | 25192 B | 1.00 | -| ReadBigUpstream | 4 KB order, read | 11,445.5 ns | 87.77 ns | 63.46 ns | 1.48 | 0.01 | 1.8921 | 0.1373 | 24920 B | 0.99 | -| | | | | | | | | | | | -| WriteBig | 4 KB order, write | 4,749.4 ns | 25.99 ns | 18.79 ns | 1.00 | 0.01 | - | - | - | NA | -| WriteBigUpstream | 4 KB order, write | 6,837.6 ns | 44.56 ns | 34.79 ns | 1.44 | 0.01 | 0.1144 | - | 1560 B | NA | -| | | | | | | | | | | | -| ReadOrder | Order, read | 621.0 ns | 5.63 ns | 4.39 ns | 1.00 | 0.01 | 0.1860 | 0.0010 | 2432 B | 1.00 | -| ReadOrderUpstream | Order, read | 791.5 ns | 8.85 ns | 5.86 ns | 1.27 | 0.01 | 0.1841 | 0.0010 | 2416 B | 0.99 | -| | | | | | | | | | | | -| WriteOrder | Order, write | 339.9 ns | 2.42 ns | 1.89 ns | 1.00 | 0.01 | - | - | - | NA | -| WriteOrderUpstream | Order, write | 597.5 ns | 3.08 ns | 2.23 ns | 1.76 | 0.01 | 0.0114 | - | 152 B | NA | -| | | | | | | | | | | | -| ReadIntDoubleMap | map, read | 598.8 ns | 7.47 ns | 5.83 ns | 1.00 | 0.01 | 0.2413 | 0.0019 | 3152 B | 1.00 | -| ReadIntDoubleMapUpstream | map, read | 1,740.0 ns | 20.98 ns | 16.38 ns | 2.91 | 0.04 | 0.7801 | 0.0172 | 10216 B | 3.24 | -| | | | | | | | | | | | -| WriteIntDoubleMap | map, write | 258.0 ns | 1.38 ns | 1.00 ns | 1.00 | 0.01 | - | - | - | NA | -| WriteIntDoubleMapUpstream | map, write | 1,102.8 ns | 8.47 ns | 6.62 ns | 4.27 | 0.03 | 0.0038 | - | 56 B | NA | -| | | | | | | | | | | | -| ReadStringMap | map, read | 2,790.7 ns | 9.54 ns | 7.45 ns | 1.00 | 0.00 | 0.8469 | 0.0305 | 11072 B | 1.00 | -| ReadStringMapUpstream | map, read | 3,899.6 ns | 33.16 ns | 25.89 ns | 1.40 | 0.01 | 1.3809 | 0.0687 | 18136 B | 1.64 | -| | | | | | | | | | | | -| WriteStringMap | map, write | 1,570.0 ns | 9.58 ns | 6.92 ns | 1.00 | 0.01 | - | - | - | NA | -| WriteStringMapUpstream | map, write | 2,935.5 ns | 23.54 ns | 18.38 ns | 1.87 | 0.01 | 0.0038 | - | 56 B | NA | +| Method | Categories | Mean | Error | StdDev | Ratio | RatioSD | Gen0 | Gen1 | Allocated | Alloc Ratio | +|-------------------------- |--------------------------- |-------------:|-----------:|----------:|------:|--------:|-------:|-------:|----------:|------------:| +| ReadDoubles | 1,000 double, read | 1,444.09 ns | 18.064 ns | 13.062 ns | 1.00 | 0.01 | 0.6180 | 0.0172 | 8080 B | 1.00 | +| ReadDoublesUpstream | 1,000 double, read | 3,232.43 ns | 15.259 ns | 10.093 ns | 2.24 | 0.02 | 0.6180 | 0.0153 | 8080 B | 1.00 | +| | | | | | | | | | | | +| WriteDoubles | 1,000 double, write | 1,053.58 ns | 7.288 ns | 5.690 ns | 1.00 | 0.01 | - | - | - | NA | +| WriteDoublesUpstream | 1,000 double, write | 1,694.99 ns | 7.314 ns | 4.838 ns | 1.61 | 0.01 | - | - | - | NA | +| | | | | | | | | | | | +| ReadInts | 100 int32, read | 181.59 ns | 1.510 ns | 1.179 ns | 1.00 | 0.01 | 0.0367 | - | 480 B | 1.00 | +| ReadIntsUpstream | 100 int32, read | 343.96 ns | 3.068 ns | 2.395 ns | 1.89 | 0.02 | 0.0367 | - | 480 B | 1.00 | +| | | | | | | | | | | | +| WriteInts | 100 int32, write | 99.40 ns | 2.844 ns | 2.056 ns | 1.00 | 0.03 | - | - | - | NA | +| WriteIntsUpstream | 100 int32, write | 227.66 ns | 1.056 ns | 0.825 ns | 2.29 | 0.05 | - | - | - | NA | +| | | | | | | | | | | | +| ReadItems | 100 structs, read | 2,261.03 ns | 24.026 ns | 15.892 ns | 1.00 | 0.01 | 0.6790 | 0.0191 | 8880 B | 1.00 | +| ReadItemsUpstream | 100 structs, read | 3,335.26 ns | 35.646 ns | 25.774 ns | 1.48 | 0.01 | 0.6790 | 0.0191 | 8880 B | 1.00 | +| | | | | | | | | | | | +| WriteItems | 100 structs, write | 1,404.08 ns | 10.068 ns | 7.860 ns | 1.00 | 0.01 | - | - | - | NA | +| WriteItemsUpstream | 100 structs, write | 1,946.34 ns | 8.240 ns | 5.958 ns | 1.39 | 0.01 | - | - | - | NA | +| | | | | | | | | | | | +| ReadBig | 4 KB order, read | 7,518.83 ns | 83.181 ns | 64.942 ns | 1.00 | 0.01 | 1.9226 | 0.1450 | 25192 B | 1.00 | +| ReadBigUpstream | 4 KB order, read | 11,428.90 ns | 130.369 ns | 94.266 ns | 1.52 | 0.02 | 1.8921 | 0.1373 | 24920 B | 0.99 | +| | | | | | | | | | | | +| WriteBig | 4 KB order, write | 4,102.92 ns | 27.765 ns | 21.677 ns | 1.00 | 0.01 | - | - | - | NA | +| WriteBigUpstream | 4 KB order, write | 6,797.33 ns | 53.665 ns | 41.898 ns | 1.66 | 0.01 | 0.1144 | - | 1560 B | NA | +| | | | | | | | | | | | +| ReadOrder | Order, read | 617.10 ns | 3.630 ns | 2.625 ns | 1.00 | 0.01 | 0.1860 | 0.0010 | 2432 B | 1.00 | +| ReadOrderUpstream | Order, read | 792.22 ns | 11.949 ns | 8.640 ns | 1.28 | 0.01 | 0.1841 | 0.0010 | 2416 B | 0.99 | +| | | | | | | | | | | | +| WriteOrder | Order, write | 296.68 ns | 3.503 ns | 2.735 ns | 1.00 | 0.01 | - | - | - | NA | +| WriteOrderUpstream | Order, write | 586.45 ns | 2.659 ns | 1.759 ns | 1.98 | 0.02 | 0.0114 | - | 152 B | NA | +| | | | | | | | | | | | +| ReadIntDoubleMap | map, read | 512.68 ns | 4.672 ns | 3.648 ns | 1.00 | 0.01 | 0.2413 | 0.0019 | 3152 B | 1.00 | +| ReadIntDoubleMapUpstream | map, read | 1,735.54 ns | 71.599 ns | 55.900 ns | 3.39 | 0.11 | 0.7801 | 0.0172 | 10216 B | 3.24 | +| | | | | | | | | | | | +| WriteIntDoubleMap | map, write | 203.04 ns | 1.936 ns | 1.280 ns | 1.00 | 0.01 | - | - | - | NA | +| WriteIntDoubleMapUpstream | map, write | 1,107.65 ns | 3.909 ns | 3.052 ns | 5.46 | 0.04 | 0.0038 | - | 56 B | NA | +| | | | | | | | | | | | +| ReadStringMap | map, read | 2,785.66 ns | 24.780 ns | 16.390 ns | 1.00 | 0.01 | 0.8469 | 0.0305 | 11072 B | 1.00 | +| ReadStringMapUpstream | map, read | 3,905.10 ns | 33.080 ns | 23.919 ns | 1.40 | 0.01 | 1.3809 | 0.0687 | 18136 B | 1.64 | +| | | | | | | | | | | | +| WriteStringMap | map, write | 1,470.10 ns | 17.829 ns | 13.919 ns | 1.00 | 0.01 | - | - | - | NA | +| WriteStringMapUpstream | map, write | 2,988.01 ns | 23.073 ns | 18.014 ns | 2.03 | 0.02 | 0.0038 | - | 56 B | NA | ## Apple M4 Pro @@ -70,46 +70,46 @@ Apple M4 Pro, 1 CPU, 14 logical and 14 physical cores IterationCount=12 LaunchCount=1 WarmupCount=6 ``` -| Method | Categories | Mean | Error | StdDev | Ratio | RatioSD | Gen0 | Gen1 | Allocated | Alloc Ratio | -|---------------------------|----------------------------|------------:|----------:|----------:|------:|--------:|-------:|-------:|----------:|------------:| -| ReadDoubles | 1,000 double, read | 1,157.7 ns | 13.61 ns | 10.62 ns | 1.00 | 0.01 | 0.9651 | 0.0267 | 8080 B | 1.00 | -| ReadDoublesUpstream | 1,000 double, read | 3,892.9 ns | 49.55 ns | 38.69 ns | 3.36 | 0.04 | 0.9613 | 0.0229 | 8080 B | 1.00 | -| | | | | | | | | | | | -| WriteDoubles | 1,000 double, write | 948.7 ns | 7.74 ns | 5.12 ns | 1.00 | 0.01 | - | - | - | NA | -| WriteDoublesUpstream | 1,000 double, write | 2,727.8 ns | 157.97 ns | 123.33 ns | 2.88 | 0.13 | - | - | - | NA | -| | | | | | | | | | | | -| ReadInts | 100 int32, read | 341.4 ns | 2.30 ns | 1.79 ns | 1.00 | 0.01 | 0.0572 | - | 480 B | 1.00 | -| ReadIntsUpstream | 100 int32, read | 280.9 ns | 3.01 ns | 2.35 ns | 0.82 | 0.01 | 0.0572 | - | 480 B | 1.00 | -| | | | | | | | | | | | -| WriteInts | 100 int32, write | 138.9 ns | 0.90 ns | 0.70 ns | 1.00 | 0.01 | - | - | - | NA | -| WriteIntsUpstream | 100 int32, write | 191.5 ns | 1.33 ns | 0.88 ns | 1.38 | 0.01 | - | - | - | NA | -| | | | | | | | | | | | -| ReadItems | 100 structs, read | 2,009.7 ns | 7.19 ns | 4.75 ns | 1.00 | 0.00 | 1.0605 | 0.0305 | 8880 B | 1.00 | -| ReadItemsUpstream | 100 structs, read | 2,572.3 ns | 27.54 ns | 21.50 ns | 1.28 | 0.01 | 1.0605 | 0.0305 | 8880 B | 1.00 | -| | | | | | | | | | | | -| WriteItems | 100 structs, write | 1,018.8 ns | 6.89 ns | 4.98 ns | 1.00 | 0.01 | - | - | - | NA | -| WriteItemsUpstream | 100 structs, write | 1,614.0 ns | 15.18 ns | 11.85 ns | 1.58 | 0.01 | - | - | - | NA | -| | | | | | | | | | | | -| ReadBig | 4 KB order, read | 6,228.0 ns | 46.18 ns | 36.06 ns | 1.00 | 0.01 | 3.0060 | 0.2365 | 25192 B | 1.00 | -| ReadBigUpstream | 4 KB order, read | 10,039.9 ns | 87.85 ns | 68.58 ns | 1.61 | 0.01 | 2.9755 | 0.2289 | 24920 B | 0.99 | -| | | | | | | | | | | | -| WriteBig | 4 KB order, write | 3,776.7 ns | 51.15 ns | 39.93 ns | 1.00 | 0.01 | - | - | - | NA | -| WriteBigUpstream | 4 KB order, write | 5,603.4 ns | 61.84 ns | 48.28 ns | 1.48 | 0.02 | 0.1831 | - | 1560 B | NA | -| | | | | | | | | | | | -| ReadOrder | Order, read | 449.6 ns | 4.99 ns | 3.90 ns | 1.00 | 0.01 | 0.2904 | 0.0024 | 2432 B | 1.00 | -| ReadOrderUpstream | Order, read | 614.0 ns | 7.91 ns | 6.17 ns | 1.37 | 0.02 | 0.2880 | 0.0019 | 2416 B | 0.99 | -| | | | | | | | | | | | -| WriteOrder | Order, write | 267.0 ns | 2.53 ns | 1.97 ns | 1.00 | 0.01 | - | - | - | NA | -| WriteOrderUpstream | Order, write | 442.1 ns | 2.40 ns | 1.88 ns | 1.66 | 0.01 | 0.0181 | - | 152 B | NA | -| | | | | | | | | | | | -| ReadIntDoubleMap | map, read | 440.3 ns | 5.00 ns | 3.90 ns | 1.00 | 0.01 | 0.3767 | 0.0043 | 3152 B | 1.00 | -| ReadIntDoubleMapUpstream | map, read | 1,759.1 ns | 17.64 ns | 12.76 ns | 4.00 | 0.04 | 1.2207 | 0.0191 | 10216 B | 3.24 | -| | | | | | | | | | | | -| WriteIntDoubleMap | map, write | 224.4 ns | 1.37 ns | 1.07 ns | 1.00 | 0.01 | - | - | - | NA | -| WriteIntDoubleMapUpstream | map, write | 789.5 ns | 5.75 ns | 4.49 ns | 3.52 | 0.03 | 0.0067 | - | 56 B | NA | -| | | | | | | | | | | | -| ReadStringMap | map, read | 2,632.0 ns | 20.10 ns | 14.54 ns | 1.00 | 0.01 | 1.3199 | 0.0496 | 11072 B | 1.00 | -| ReadStringMapUpstream | map, read | 3,342.2 ns | 36.69 ns | 28.65 ns | 1.27 | 0.01 | 2.1667 | 0.1068 | 18136 B | 1.64 | -| | | | | | | | | | | | -| WriteStringMap | map, write | 1,096.7 ns | 9.43 ns | 7.36 ns | 1.00 | 0.01 | - | - | - | NA | -| WriteStringMapUpstream | map, write | 1,961.1 ns | 17.54 ns | 12.68 ns | 1.79 | 0.02 | 0.0038 | - | 56 B | NA | +| Method | Categories | Mean | Error | StdDev | Ratio | RatioSD | Gen0 | Gen1 | Allocated | Alloc Ratio | +|-------------------------- |--------------------------- |------------:|-----------:|-----------:|------:|--------:|-------:|-------:|----------:|------------:| +| ReadDoubles | 1,000 double, read | 1,142.81 ns | 11.968 ns | 9.344 ns | 1.00 | 0.01 | 0.9651 | 0.0267 | 8080 B | 1.00 | +| ReadDoublesUpstream | 1,000 double, read | 3,907.89 ns | 34.736 ns | 25.117 ns | 3.42 | 0.03 | 0.9613 | 0.0229 | 8080 B | 1.00 | +| | | | | | | | | | | | +| WriteDoubles | 1,000 double, write | 945.15 ns | 4.984 ns | 3.891 ns | 1.00 | 0.01 | - | - | - | NA | +| WriteDoublesUpstream | 1,000 double, write | 2,901.35 ns | 282.021 ns | 220.184 ns | 3.07 | 0.22 | - | - | - | NA | +| | | | | | | | | | | | +| ReadInts | 100 int32, read | 116.95 ns | 0.705 ns | 0.550 ns | 1.00 | 0.01 | 0.0572 | - | 480 B | 1.00 | +| ReadIntsUpstream | 100 int32, read | 285.26 ns | 3.287 ns | 2.567 ns | 2.44 | 0.02 | 0.0572 | - | 480 B | 1.00 | +| | | | | | | | | | | | +| WriteInts | 100 int32, write | 90.48 ns | 0.484 ns | 0.320 ns | 1.00 | 0.00 | - | - | - | NA | +| WriteIntsUpstream | 100 int32, write | 193.02 ns | 1.293 ns | 1.010 ns | 2.13 | 0.01 | - | - | - | NA | +| | | | | | | | | | | | +| ReadItems | 100 structs, read | 2,039.34 ns | 15.968 ns | 12.467 ns | 1.00 | 0.01 | 1.0605 | 0.0305 | 8880 B | 1.00 | +| ReadItemsUpstream | 100 structs, read | 2,567.12 ns | 20.059 ns | 15.661 ns | 1.26 | 0.01 | 1.0605 | 0.0305 | 8880 B | 1.00 | +| | | | | | | | | | | | +| WriteItems | 100 structs, write | 1,070.47 ns | 10.152 ns | 7.926 ns | 1.00 | 0.01 | - | - | - | NA | +| WriteItemsUpstream | 100 structs, write | 1,643.61 ns | 12.776 ns | 9.238 ns | 1.54 | 0.01 | - | - | - | NA | +| | | | | | | | | | | | +| ReadBig | 4 KB order, read | 6,103.34 ns | 66.873 ns | 52.210 ns | 1.00 | 0.01 | 3.0060 | 0.2365 | 25192 B | 1.00 | +| ReadBigUpstream | 4 KB order, read | 9,896.98 ns | 112.872 ns | 88.123 ns | 1.62 | 0.02 | 2.9755 | 0.2289 | 24920 B | 0.99 | +| | | | | | | | | | | | +| WriteBig | 4 KB order, write | 3,375.08 ns | 49.971 ns | 33.052 ns | 1.00 | 0.01 | - | - | - | NA | +| WriteBigUpstream | 4 KB order, write | 5,584.09 ns | 48.599 ns | 37.943 ns | 1.65 | 0.02 | 0.1831 | - | 1560 B | NA | +| | | | | | | | | | | | +| ReadOrder | Order, read | 445.37 ns | 2.006 ns | 1.450 ns | 1.00 | 0.00 | 0.2904 | 0.0024 | 2432 B | 1.00 | +| ReadOrderUpstream | Order, read | 603.83 ns | 4.356 ns | 3.401 ns | 1.36 | 0.01 | 0.2880 | 0.0019 | 2416 B | 0.99 | +| | | | | | | | | | | | +| WriteOrder | Order, write | 225.60 ns | 1.300 ns | 1.015 ns | 1.00 | 0.01 | - | - | - | NA | +| WriteOrderUpstream | Order, write | 455.65 ns | 4.213 ns | 3.290 ns | 2.02 | 0.02 | 0.0181 | - | 152 B | NA | +| | | | | | | | | | | | +| ReadIntDoubleMap | map, read | 378.45 ns | 2.344 ns | 1.830 ns | 1.00 | 0.01 | 0.3767 | 0.0043 | 3152 B | 1.00 | +| ReadIntDoubleMapUpstream | map, read | 1,757.56 ns | 13.618 ns | 10.632 ns | 4.64 | 0.03 | 1.2207 | 0.0191 | 10216 B | 3.24 | +| | | | | | | | | | | | +| WriteIntDoubleMap | map, write | 173.26 ns | 1.232 ns | 0.962 ns | 1.00 | 0.01 | - | - | - | NA | +| WriteIntDoubleMapUpstream | map, write | 792.33 ns | 5.310 ns | 4.146 ns | 4.57 | 0.03 | 0.0067 | - | 56 B | NA | +| | | | | | | | | | | | +| ReadStringMap | map, read | 2,504.68 ns | 18.930 ns | 14.779 ns | 1.00 | 0.01 | 1.3199 | 0.0496 | 11072 B | 1.00 | +| ReadStringMapUpstream | map, read | 3,349.69 ns | 20.477 ns | 15.987 ns | 1.34 | 0.01 | 2.1667 | 0.1068 | 18136 B | 1.64 | +| | | | | | | | | | | | +| WriteStringMap | map, write | 983.06 ns | 9.507 ns | 7.422 ns | 1.00 | 0.01 | - | - | - | NA | +| WriteStringMapUpstream | map, write | 1,967.88 ns | 15.787 ns | 12.326 ns | 2.00 | 0.02 | 0.0038 | - | 56 B | NA | diff --git a/version b/version index bc80560..dc1e644 100644 --- a/version +++ b/version @@ -1 +1 @@ -1.5.0 +1.6.0 From b2cb4c15f00f5321e478ea34b15f812f6c915bab Mon Sep 17 00:00:00 2001 From: mircodz Date: Fri, 2 Oct 2026 14:32:04 +0200 Subject: [PATCH 2/4] perf(runtime): faster strings ASCII strings, the common case, are written in one narrowing pass, since their byte count is their length, and read by widening, which Latin-1 does for ASCII exactly as UTF-8 does. Other strings up to 5,461 characters are encoded in one pass too, after room for the longest length prefix they can need. The bytes written and the strings read are unchanged. --- Bond.Runtime/CompactBinaryReader.cs | 5 ++- Bond.Runtime/CompactBinaryWriter.cs | 65 +++++++++++++++++++++++++++-- 2 files changed, 65 insertions(+), 5 deletions(-) diff --git a/Bond.Runtime/CompactBinaryReader.cs b/Bond.Runtime/CompactBinaryReader.cs index e0427f8..2792498 100644 --- a/Bond.Runtime/CompactBinaryReader.cs +++ b/Bond.Runtime/CompactBinaryReader.cs @@ -143,7 +143,10 @@ public double ReadDouble(WireType type) => public string ReadString(WireType type) { Expect(type, WireType.String); - return Encoding.UTF8.GetString(Take(ReadLength())); + var bytes = Take(ReadLength()); + + // ASCII, the common case, is widened as Latin-1, which decodes it as UTF-8 does, only faster. + return Ascii.IsValid(bytes) ? Encoding.Latin1.GetString(bytes) : Encoding.UTF8.GetString(bytes); } /// Reads a UTF-16 string. diff --git a/Bond.Runtime/CompactBinaryWriter.cs b/Bond.Runtime/CompactBinaryWriter.cs index 2c24449..6c1fc53 100644 --- a/Bond.Runtime/CompactBinaryWriter.cs +++ b/Bond.Runtime/CompactBinaryWriter.cs @@ -6,6 +6,7 @@ using System.Runtime.InteropServices; using System.Runtime.Intrinsics.X86; using System.Text; +using System.Text.Unicode; namespace BondTools.Runtime; @@ -17,6 +18,9 @@ public ref struct CompactBinaryWriter private const int MaxVarIntSize = 10; + // The longest string whose UTF-8 bytes, at most 3 per character, have a length of at most two bytes. + private const int MaxSinglePassLength = 0x3FFF / 3; + private readonly IBufferWriter _output; private Span _buffer; private int _position; @@ -109,12 +113,65 @@ public void WriteDouble(double value) /// Invalid UTF-16 is replaced with U+FFFD, as does. public void WriteString(string value) { - var size = Encoding.UTF8.GetByteCount(value); - WriteVarUInt64((uint)size); + // ASCII, the common case, takes one narrowing pass: its byte count is its length. + var length = value.Length; + if (length < 0x4000) + { + var prefix = length < 0x80 ? 1 : 2; + var destination = Reserve(prefix + length); + if (Ascii.FromUtf16(value, destination[prefix..], out _) == OperationStatus.Done) + { + WritePrefix(destination, prefix, length); + _position += prefix + length; + return; + } + } + + WriteUtf8String(value); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private void WriteUtf8String(string value) + { + var length = value.Length; + if (length <= MaxSinglePassLength) + { + // Encoded once, after room for the longest prefix it can need, then moved back if the prefix is shorter. + var room = length * 3; + var prefix = room < 0x80 ? 1 : 2; + var destination = Reserve(prefix + room); + Utf8.FromUtf16(value, destination[prefix..], out _, out var size, replaceInvalidSequences: true); + if (prefix == 2 && size < 0x80) + { + destination.Slice(2, size).CopyTo(destination[1..]); + prefix = 1; + } + + WritePrefix(destination, prefix, size); + _position += prefix + size; + return; + } + + var count = Encoding.UTF8.GetByteCount(value); + WriteVarUInt64((uint)count); // Reserve before reading the position: it may move to a new buffer and reset the position. - var destination = Reserve(size); - _position += Encoding.UTF8.GetBytes(value, destination); + var bytes = Reserve(count); + _position += Encoding.UTF8.GetBytes(value, bytes); + } + + // A string's length in bytes as a one- or two-byte varint. + private static void WritePrefix(Span destination, int prefix, int size) + { + if (prefix == 1) + { + destination[0] = (byte)size; + } + else + { + destination[0] = (byte)(size | 0x80); + destination[1] = (byte)(size >> 7); + } } /// Writes a UTF-16 string, prefixed with its length in characters. From 2fed431136384666974fac8d11e5ed0a441cdbaa Mon Sep 17 00:00:00 2001 From: mircodz Date: Fri, 2 Oct 2026 14:32:15 +0200 Subject: [PATCH 3/4] perf(runtime): move rare paths out of line Buffer growth and the exceptions for malformed payloads were inlined into every generated read and write method. Out of line, the generated code is 15% smaller, and writers 25%. --- Bond.Runtime/CompactBinaryReader.cs | 18 +++++++++++++++--- Bond.Runtime/CompactBinaryWriter.cs | 25 ++++++++++++++++++------- 2 files changed, 33 insertions(+), 10 deletions(-) diff --git a/Bond.Runtime/CompactBinaryReader.cs b/Bond.Runtime/CompactBinaryReader.cs index 2792498..5040c8b 100644 --- a/Bond.Runtime/CompactBinaryReader.cs +++ b/Bond.Runtime/CompactBinaryReader.cs @@ -240,7 +240,8 @@ public void Skip(WireType type) _depth--; break; default: - throw new InvalidDataException($"Invalid Bond wire type {(byte)type}."); + ThrowUnknownType(type); + break; } } @@ -270,7 +271,7 @@ private void Enter() { if (++_depth > MaxDepth) { - throw new InvalidDataException($"Bond payload is nested deeper than {MaxDepth} levels."); + ThrowTooDeep(); } } @@ -334,7 +335,7 @@ private int ReadLength() var length = ReadVarUInt32(); if (length > int.MaxValue) { - throw new InvalidDataException("Bond length is out of range."); + ThrowLengthOutOfRange(); } return (int)length; @@ -541,4 +542,15 @@ private ulong ReadVarUInt64Checked() [DoesNotReturn] private static void ThrowInvalidType(WireType expected, WireType actual) => throw new InvalidDataException($"Invalid Bond wire type {actual}, expected {expected}."); + + [DoesNotReturn] + private static void ThrowUnknownType(WireType type) => + throw new InvalidDataException($"Invalid Bond wire type {(byte)type}."); + + [DoesNotReturn] + private static void ThrowTooDeep() => + throw new InvalidDataException($"Bond payload is nested deeper than {MaxDepth} levels."); + + [DoesNotReturn] + private static void ThrowLengthOutOfRange() => throw new InvalidDataException("Bond length is out of range."); } diff --git a/Bond.Runtime/CompactBinaryWriter.cs b/Bond.Runtime/CompactBinaryWriter.cs index 6c1fc53..3ec2861 100644 --- a/Bond.Runtime/CompactBinaryWriter.cs +++ b/Bond.Runtime/CompactBinaryWriter.cs @@ -1,6 +1,7 @@ using System; using System.Buffers; using System.Buffers.Binary; +using System.Diagnostics.CodeAnalysis; using System.Numerics; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; @@ -276,18 +277,28 @@ private Span Reserve(int size) { if (_buffer.Length - _position < size) { - _output.Advance(_position); - _buffer = _output.GetSpan(Math.Max(size, MinimumBufferSize)); - _position = 0; - if (_buffer.Length < size) - { - throw new InvalidOperationException("The output returned a buffer smaller than requested."); - } + Grow(size); } return _buffer[_position..]; } + [MethodImpl(MethodImplOptions.NoInlining)] + private void Grow(int size) + { + _output.Advance(_position); + _buffer = _output.GetSpan(Math.Max(size, MinimumBufferSize)); + _position = 0; + if (_buffer.Length < size) + { + ThrowBufferTooSmall(); + } + } + + [DoesNotReturn] + private static void ThrowBufferTooSmall() => + throw new InvalidOperationException("The output returned a buffer smaller than requested."); + private static uint ZigZag(int value) => (uint)((value << 1) ^ (value >> 31)); private static ulong ZigZag(long value) => (ulong)((value << 1) ^ (value >> 63)); From 6868e7850378c606815ee9a6549ddbd2920aa60d Mon Sep 17 00:00:00 2001 From: mircodz Date: Fri, 2 Oct 2026 16:11:14 +0200 Subject: [PATCH 4/4] chore(benchmarks): launch each benchmark three times Single launches varied by up to 15% from one process to the next. The README and RESULTS.md are refreshed with the varint, string and rare-path changes. --- Bond.Runtime/README.md | 28 +++--- benchmarks/Benchmarks.cs | 2 +- benchmarks/RESULTS.md | 180 +++++++++++++++++++-------------------- 3 files changed, 105 insertions(+), 105 deletions(-) diff --git a/Bond.Runtime/README.md b/Bond.Runtime/README.md index b783936..2ab49fa 100644 --- a/Bond.Runtime/README.md +++ b/Bond.Runtime/README.md @@ -33,22 +33,22 @@ Mean time per call on an Intel i7-12700KF with .NET 10; upstream is Bond.Runtime | Write | bond-tools | upstream | Δ | |---|--:|--:|--:| -| Order with nested structs, strings and maps | 297 ns | 586 ns | -49% | -| 100 small structs | 1.40 µs | 1.95 µs | -28% | -| 100 `int32` | 99 ns | 228 ns | -56% | -| 1,000 `double` | 1.05 µs | 1.69 µs | -38% | -| `map`, 100 entries | 1.47 µs | 2.99 µs | -51% | -| `map`, 100 entries | 203 ns | 1.11 µs | -82% | -| 4 KB order using most Bond types | 4.10 µs | 6.80 µs | -40% | +| Order with nested structs, strings and maps | 181 ns | 598 ns | -70% | +| 100 small structs | 993 ns | 1.99 µs | -50% | +| 100 `int32` | 99 ns | 231 ns | -57% | +| 1,000 `double` | 1.05 µs | 1.90 µs | -45% | +| `map`, 100 entries | 650 ns | 2.93 µs | -78% | +| `map`, 100 entries | 209 ns | 1.13 µs | -82% | +| 4 KB order using most Bond types | 3.47 µs | 6.76 µs | -49% | | Read | bond-tools | upstream | Δ | |---|--:|--:|--:| -| Order with nested structs, strings and maps | 617 ns | 792 ns | -22% | -| 100 small structs | 2.26 µs | 3.34 µs | -32% | -| 100 `int32` | 182 ns | 344 ns | -47% | -| 1,000 `double` | 1.44 µs | 3.23 µs | -55% | -| `map`, 100 entries | 2.79 µs | 3.91 µs | -29% | -| `map`, 100 entries | 513 ns | 1.74 µs | -70% | -| 4 KB order using most Bond types | 7.52 µs | 11.4 µs | -34% | +| Order with nested structs, strings and maps | 518 ns | 787 ns | -34% | +| 100 small structs | 1.96 µs | 3.21 µs | -39% | +| 100 `int32` | 180 ns | 343 ns | -48% | +| 1,000 `double` | 1.44 µs | 3.04 µs | -53% | +| `map`, 100 entries | 2.34 µs | 3.89 µs | -40% | +| `map`, 100 entries | 518 ns | 1.71 µs | -70% | +| 4 KB order using most Bond types | 7.53 µs | 11.5 µs | -34% | `dotnet run -c Release --project benchmarks` runs them. [benchmarks/RESULTS.md](../benchmarks/RESULTS.md) has the full results, also for an Apple M4 Pro. diff --git a/benchmarks/Benchmarks.cs b/benchmarks/Benchmarks.cs index 98c3554..049b21b 100644 --- a/benchmarks/Benchmarks.cs +++ b/benchmarks/Benchmarks.cs @@ -13,7 +13,7 @@ [MemoryDiagnoser] [GroupBenchmarksBy(BenchmarkLogicalGroupRule.ByCategory)] [CategoriesColumn] -[SimpleJob(launchCount: 1, warmupCount: 6, iterationCount: 12)] +[SimpleJob(launchCount: 3, warmupCount: 5, iterationCount: 10)] public class Benchmarks { private readonly ArrayBufferWriter _output = new(64 * 1024); diff --git a/benchmarks/RESULTS.md b/benchmarks/RESULTS.md index dd3b942..439d0bc 100644 --- a/benchmarks/RESULTS.md +++ b/benchmarks/RESULTS.md @@ -9,54 +9,54 @@ BenchmarkDotNet v0.15.8, Linux Arch Linux 12th Gen Intel Core i7-12700KF 0.80GHz, 1 CPU, 20 logical and 12 physical cores .NET SDK 10.0.110 [Host] : .NET 10.0.10 (10.0.10, 42.42.42.42424), X64 RyuJIT x86-64-v3 - Job-DRYOBN : .NET 10.0.10 (10.0.10, 42.42.42.42424), X64 RyuJIT x86-64-v3 + Job-DYZKOT : .NET 10.0.10 (10.0.10, 42.42.42.42424), X64 RyuJIT x86-64-v3 -IterationCount=12 LaunchCount=1 WarmupCount=6 +IterationCount=10 LaunchCount=3 WarmupCount=5 ``` -| Method | Categories | Mean | Error | StdDev | Ratio | RatioSD | Gen0 | Gen1 | Allocated | Alloc Ratio | -|-------------------------- |--------------------------- |-------------:|-----------:|----------:|------:|--------:|-------:|-------:|----------:|------------:| -| ReadDoubles | 1,000 double, read | 1,444.09 ns | 18.064 ns | 13.062 ns | 1.00 | 0.01 | 0.6180 | 0.0172 | 8080 B | 1.00 | -| ReadDoublesUpstream | 1,000 double, read | 3,232.43 ns | 15.259 ns | 10.093 ns | 2.24 | 0.02 | 0.6180 | 0.0153 | 8080 B | 1.00 | -| | | | | | | | | | | | -| WriteDoubles | 1,000 double, write | 1,053.58 ns | 7.288 ns | 5.690 ns | 1.00 | 0.01 | - | - | - | NA | -| WriteDoublesUpstream | 1,000 double, write | 1,694.99 ns | 7.314 ns | 4.838 ns | 1.61 | 0.01 | - | - | - | NA | -| | | | | | | | | | | | -| ReadInts | 100 int32, read | 181.59 ns | 1.510 ns | 1.179 ns | 1.00 | 0.01 | 0.0367 | - | 480 B | 1.00 | -| ReadIntsUpstream | 100 int32, read | 343.96 ns | 3.068 ns | 2.395 ns | 1.89 | 0.02 | 0.0367 | - | 480 B | 1.00 | -| | | | | | | | | | | | -| WriteInts | 100 int32, write | 99.40 ns | 2.844 ns | 2.056 ns | 1.00 | 0.03 | - | - | - | NA | -| WriteIntsUpstream | 100 int32, write | 227.66 ns | 1.056 ns | 0.825 ns | 2.29 | 0.05 | - | - | - | NA | -| | | | | | | | | | | | -| ReadItems | 100 structs, read | 2,261.03 ns | 24.026 ns | 15.892 ns | 1.00 | 0.01 | 0.6790 | 0.0191 | 8880 B | 1.00 | -| ReadItemsUpstream | 100 structs, read | 3,335.26 ns | 35.646 ns | 25.774 ns | 1.48 | 0.01 | 0.6790 | 0.0191 | 8880 B | 1.00 | -| | | | | | | | | | | | -| WriteItems | 100 structs, write | 1,404.08 ns | 10.068 ns | 7.860 ns | 1.00 | 0.01 | - | - | - | NA | -| WriteItemsUpstream | 100 structs, write | 1,946.34 ns | 8.240 ns | 5.958 ns | 1.39 | 0.01 | - | - | - | NA | -| | | | | | | | | | | | -| ReadBig | 4 KB order, read | 7,518.83 ns | 83.181 ns | 64.942 ns | 1.00 | 0.01 | 1.9226 | 0.1450 | 25192 B | 1.00 | -| ReadBigUpstream | 4 KB order, read | 11,428.90 ns | 130.369 ns | 94.266 ns | 1.52 | 0.02 | 1.8921 | 0.1373 | 24920 B | 0.99 | -| | | | | | | | | | | | -| WriteBig | 4 KB order, write | 4,102.92 ns | 27.765 ns | 21.677 ns | 1.00 | 0.01 | - | - | - | NA | -| WriteBigUpstream | 4 KB order, write | 6,797.33 ns | 53.665 ns | 41.898 ns | 1.66 | 0.01 | 0.1144 | - | 1560 B | NA | -| | | | | | | | | | | | -| ReadOrder | Order, read | 617.10 ns | 3.630 ns | 2.625 ns | 1.00 | 0.01 | 0.1860 | 0.0010 | 2432 B | 1.00 | -| ReadOrderUpstream | Order, read | 792.22 ns | 11.949 ns | 8.640 ns | 1.28 | 0.01 | 0.1841 | 0.0010 | 2416 B | 0.99 | -| | | | | | | | | | | | -| WriteOrder | Order, write | 296.68 ns | 3.503 ns | 2.735 ns | 1.00 | 0.01 | - | - | - | NA | -| WriteOrderUpstream | Order, write | 586.45 ns | 2.659 ns | 1.759 ns | 1.98 | 0.02 | 0.0114 | - | 152 B | NA | -| | | | | | | | | | | | -| ReadIntDoubleMap | map, read | 512.68 ns | 4.672 ns | 3.648 ns | 1.00 | 0.01 | 0.2413 | 0.0019 | 3152 B | 1.00 | -| ReadIntDoubleMapUpstream | map, read | 1,735.54 ns | 71.599 ns | 55.900 ns | 3.39 | 0.11 | 0.7801 | 0.0172 | 10216 B | 3.24 | -| | | | | | | | | | | | -| WriteIntDoubleMap | map, write | 203.04 ns | 1.936 ns | 1.280 ns | 1.00 | 0.01 | - | - | - | NA | -| WriteIntDoubleMapUpstream | map, write | 1,107.65 ns | 3.909 ns | 3.052 ns | 5.46 | 0.04 | 0.0038 | - | 56 B | NA | -| | | | | | | | | | | | -| ReadStringMap | map, read | 2,785.66 ns | 24.780 ns | 16.390 ns | 1.00 | 0.01 | 0.8469 | 0.0305 | 11072 B | 1.00 | -| ReadStringMapUpstream | map, read | 3,905.10 ns | 33.080 ns | 23.919 ns | 1.40 | 0.01 | 1.3809 | 0.0687 | 18136 B | 1.64 | -| | | | | | | | | | | | -| WriteStringMap | map, write | 1,470.10 ns | 17.829 ns | 13.919 ns | 1.00 | 0.01 | - | - | - | NA | -| WriteStringMapUpstream | map, write | 2,988.01 ns | 23.073 ns | 18.014 ns | 2.03 | 0.02 | 0.0038 | - | 56 B | NA | +| Method | Categories | Mean | Error | StdDev | Median | Ratio | RatioSD | Gen0 | Gen1 | Allocated | Alloc Ratio | +|-------------------------- |--------------------------- |-------------:|-----------:|-----------:|-------------:|------:|--------:|-------:|-------:|----------:|------------:| +| ReadDoubles | 1,000 double, read | 1,441.67 ns | 5.078 ns | 7.118 ns | 1,441.19 ns | 1.00 | 0.01 | 0.6180 | 0.0172 | 8080 B | 1.00 | +| ReadDoublesUpstream | 1,000 double, read | 3,044.54 ns | 13.162 ns | 18.877 ns | 3,044.00 ns | 2.11 | 0.02 | 0.6180 | 0.0153 | 8080 B | 1.00 | +| | | | | | | | | | | | | +| WriteDoubles | 1,000 double, write | 1,050.29 ns | 5.292 ns | 7.921 ns | 1,050.99 ns | 1.00 | 0.01 | - | - | - | NA | +| WriteDoublesUpstream | 1,000 double, write | 1,904.66 ns | 7.099 ns | 10.406 ns | 1,903.75 ns | 1.81 | 0.02 | - | - | - | NA | +| | | | | | | | | | | | | +| ReadInts | 100 int32, read | 179.54 ns | 1.413 ns | 2.026 ns | 179.11 ns | 1.00 | 0.02 | 0.0367 | - | 480 B | 1.00 | +| ReadIntsUpstream | 100 int32, read | 342.79 ns | 1.633 ns | 2.444 ns | 343.62 ns | 1.91 | 0.02 | 0.0367 | - | 480 B | 1.00 | +| | | | | | | | | | | | | +| WriteInts | 100 int32, write | 98.77 ns | 0.512 ns | 0.751 ns | 98.80 ns | 1.00 | 0.01 | - | - | - | NA | +| WriteIntsUpstream | 100 int32, write | 230.72 ns | 0.783 ns | 1.123 ns | 230.53 ns | 2.34 | 0.02 | - | - | - | NA | +| | | | | | | | | | | | | +| ReadItems | 100 structs, read | 1,962.09 ns | 16.958 ns | 24.857 ns | 1,965.10 ns | 1.00 | 0.02 | 0.6790 | 0.0191 | 8880 B | 1.00 | +| ReadItemsUpstream | 100 structs, read | 3,205.02 ns | 22.337 ns | 33.434 ns | 3,206.47 ns | 1.63 | 0.03 | 0.6790 | 0.0191 | 8880 B | 1.00 | +| | | | | | | | | | | | | +| WriteItems | 100 structs, write | 992.57 ns | 27.922 ns | 41.793 ns | 969.76 ns | 1.00 | 0.06 | - | - | - | NA | +| WriteItemsUpstream | 100 structs, write | 1,985.76 ns | 9.450 ns | 14.145 ns | 1,983.90 ns | 2.00 | 0.08 | - | - | - | NA | +| | | | | | | | | | | | | +| ReadBig | 4 KB order, read | 7,527.80 ns | 54.761 ns | 80.268 ns | 7,532.36 ns | 1.00 | 0.01 | 1.9226 | 0.1450 | 25192 B | 1.00 | +| ReadBigUpstream | 4 KB order, read | 11,480.95 ns | 120.291 ns | 172.518 ns | 11,457.14 ns | 1.53 | 0.03 | 1.8921 | 0.1373 | 24920 B | 0.99 | +| | | | | | | | | | | | | +| WriteBig | 4 KB order, write | 3,469.51 ns | 36.436 ns | 54.536 ns | 3,445.88 ns | 1.00 | 0.02 | - | - | - | NA | +| WriteBigUpstream | 4 KB order, write | 6,760.69 ns | 55.463 ns | 81.298 ns | 6,765.48 ns | 1.95 | 0.04 | 0.1144 | - | 1560 B | NA | +| | | | | | | | | | | | | +| ReadOrder | Order, read | 518.38 ns | 4.305 ns | 6.311 ns | 518.14 ns | 1.00 | 0.02 | 0.1860 | 0.0010 | 2432 B | 1.00 | +| ReadOrderUpstream | Order, read | 787.29 ns | 5.869 ns | 8.602 ns | 787.70 ns | 1.52 | 0.02 | 0.1841 | 0.0010 | 2416 B | 0.99 | +| | | | | | | | | | | | | +| WriteOrder | Order, write | 180.70 ns | 3.550 ns | 5.204 ns | 180.47 ns | 1.00 | 0.04 | - | - | - | NA | +| WriteOrderUpstream | Order, write | 597.60 ns | 2.156 ns | 3.092 ns | 597.74 ns | 3.31 | 0.10 | 0.0114 | - | 152 B | NA | +| | | | | | | | | | | | | +| ReadIntDoubleMap | map, read | 518.09 ns | 10.421 ns | 15.274 ns | 522.63 ns | 1.00 | 0.04 | 0.2413 | 0.0019 | 3152 B | 1.00 | +| ReadIntDoubleMapUpstream | map, read | 1,714.70 ns | 12.154 ns | 18.192 ns | 1,717.68 ns | 3.31 | 0.10 | 0.7801 | 0.0172 | 10216 B | 3.24 | +| | | | | | | | | | | | | +| WriteIntDoubleMap | map, write | 208.77 ns | 1.145 ns | 1.679 ns | 208.37 ns | 1.00 | 0.01 | - | - | - | NA | +| WriteIntDoubleMapUpstream | map, write | 1,128.80 ns | 4.983 ns | 7.458 ns | 1,130.04 ns | 5.41 | 0.06 | 0.0038 | - | 56 B | NA | +| | | | | | | | | | | | | +| ReadStringMap | map, read | 2,344.33 ns | 20.571 ns | 29.503 ns | 2,351.01 ns | 1.00 | 0.02 | 0.8469 | 0.0305 | 11072 B | 1.00 | +| ReadStringMapUpstream | map, read | 3,892.37 ns | 18.612 ns | 26.693 ns | 3,886.70 ns | 1.66 | 0.02 | 1.3809 | 0.0687 | 18136 B | 1.64 | +| | | | | | | | | | | | | +| WriteStringMap | map, write | 649.71 ns | 4.318 ns | 6.463 ns | 650.90 ns | 1.00 | 0.01 | - | - | - | NA | +| WriteStringMapUpstream | map, write | 2,927.56 ns | 11.762 ns | 17.241 ns | 2,929.07 ns | 4.51 | 0.05 | 0.0038 | - | 56 B | NA | ## Apple M4 Pro @@ -65,51 +65,51 @@ BenchmarkDotNet v0.15.8, macOS Tahoe 26.2 (25C56) [Darwin 25.2.0] Apple M4 Pro, 1 CPU, 14 logical and 14 physical cores .NET SDK 10.0.103 [Host] : .NET 10.0.3 (10.0.3, 10.0.326.7603), Arm64 RyuJIT armv8.0-a - Job-DRYOBN : .NET 10.0.3 (10.0.3, 10.0.326.7603), Arm64 RyuJIT armv8.0-a + Job-DYZKOT : .NET 10.0.3 (10.0.3, 10.0.326.7603), Arm64 RyuJIT armv8.0-a -IterationCount=12 LaunchCount=1 WarmupCount=6 +IterationCount=10 LaunchCount=3 WarmupCount=5 ``` -| Method | Categories | Mean | Error | StdDev | Ratio | RatioSD | Gen0 | Gen1 | Allocated | Alloc Ratio | -|-------------------------- |--------------------------- |------------:|-----------:|-----------:|------:|--------:|-------:|-------:|----------:|------------:| -| ReadDoubles | 1,000 double, read | 1,142.81 ns | 11.968 ns | 9.344 ns | 1.00 | 0.01 | 0.9651 | 0.0267 | 8080 B | 1.00 | -| ReadDoublesUpstream | 1,000 double, read | 3,907.89 ns | 34.736 ns | 25.117 ns | 3.42 | 0.03 | 0.9613 | 0.0229 | 8080 B | 1.00 | -| | | | | | | | | | | | -| WriteDoubles | 1,000 double, write | 945.15 ns | 4.984 ns | 3.891 ns | 1.00 | 0.01 | - | - | - | NA | -| WriteDoublesUpstream | 1,000 double, write | 2,901.35 ns | 282.021 ns | 220.184 ns | 3.07 | 0.22 | - | - | - | NA | -| | | | | | | | | | | | -| ReadInts | 100 int32, read | 116.95 ns | 0.705 ns | 0.550 ns | 1.00 | 0.01 | 0.0572 | - | 480 B | 1.00 | -| ReadIntsUpstream | 100 int32, read | 285.26 ns | 3.287 ns | 2.567 ns | 2.44 | 0.02 | 0.0572 | - | 480 B | 1.00 | -| | | | | | | | | | | | -| WriteInts | 100 int32, write | 90.48 ns | 0.484 ns | 0.320 ns | 1.00 | 0.00 | - | - | - | NA | -| WriteIntsUpstream | 100 int32, write | 193.02 ns | 1.293 ns | 1.010 ns | 2.13 | 0.01 | - | - | - | NA | -| | | | | | | | | | | | -| ReadItems | 100 structs, read | 2,039.34 ns | 15.968 ns | 12.467 ns | 1.00 | 0.01 | 1.0605 | 0.0305 | 8880 B | 1.00 | -| ReadItemsUpstream | 100 structs, read | 2,567.12 ns | 20.059 ns | 15.661 ns | 1.26 | 0.01 | 1.0605 | 0.0305 | 8880 B | 1.00 | -| | | | | | | | | | | | -| WriteItems | 100 structs, write | 1,070.47 ns | 10.152 ns | 7.926 ns | 1.00 | 0.01 | - | - | - | NA | -| WriteItemsUpstream | 100 structs, write | 1,643.61 ns | 12.776 ns | 9.238 ns | 1.54 | 0.01 | - | - | - | NA | -| | | | | | | | | | | | -| ReadBig | 4 KB order, read | 6,103.34 ns | 66.873 ns | 52.210 ns | 1.00 | 0.01 | 3.0060 | 0.2365 | 25192 B | 1.00 | -| ReadBigUpstream | 4 KB order, read | 9,896.98 ns | 112.872 ns | 88.123 ns | 1.62 | 0.02 | 2.9755 | 0.2289 | 24920 B | 0.99 | -| | | | | | | | | | | | -| WriteBig | 4 KB order, write | 3,375.08 ns | 49.971 ns | 33.052 ns | 1.00 | 0.01 | - | - | - | NA | -| WriteBigUpstream | 4 KB order, write | 5,584.09 ns | 48.599 ns | 37.943 ns | 1.65 | 0.02 | 0.1831 | - | 1560 B | NA | -| | | | | | | | | | | | -| ReadOrder | Order, read | 445.37 ns | 2.006 ns | 1.450 ns | 1.00 | 0.00 | 0.2904 | 0.0024 | 2432 B | 1.00 | -| ReadOrderUpstream | Order, read | 603.83 ns | 4.356 ns | 3.401 ns | 1.36 | 0.01 | 0.2880 | 0.0019 | 2416 B | 0.99 | -| | | | | | | | | | | | -| WriteOrder | Order, write | 225.60 ns | 1.300 ns | 1.015 ns | 1.00 | 0.01 | - | - | - | NA | -| WriteOrderUpstream | Order, write | 455.65 ns | 4.213 ns | 3.290 ns | 2.02 | 0.02 | 0.0181 | - | 152 B | NA | -| | | | | | | | | | | | -| ReadIntDoubleMap | map, read | 378.45 ns | 2.344 ns | 1.830 ns | 1.00 | 0.01 | 0.3767 | 0.0043 | 3152 B | 1.00 | -| ReadIntDoubleMapUpstream | map, read | 1,757.56 ns | 13.618 ns | 10.632 ns | 4.64 | 0.03 | 1.2207 | 0.0191 | 10216 B | 3.24 | -| | | | | | | | | | | | -| WriteIntDoubleMap | map, write | 173.26 ns | 1.232 ns | 0.962 ns | 1.00 | 0.01 | - | - | - | NA | -| WriteIntDoubleMapUpstream | map, write | 792.33 ns | 5.310 ns | 4.146 ns | 4.57 | 0.03 | 0.0067 | - | 56 B | NA | -| | | | | | | | | | | | -| ReadStringMap | map, read | 2,504.68 ns | 18.930 ns | 14.779 ns | 1.00 | 0.01 | 1.3199 | 0.0496 | 11072 B | 1.00 | -| ReadStringMapUpstream | map, read | 3,349.69 ns | 20.477 ns | 15.987 ns | 1.34 | 0.01 | 2.1667 | 0.1068 | 18136 B | 1.64 | -| | | | | | | | | | | | -| WriteStringMap | map, write | 983.06 ns | 9.507 ns | 7.422 ns | 1.00 | 0.01 | - | - | - | NA | -| WriteStringMapUpstream | map, write | 1,967.88 ns | 15.787 ns | 12.326 ns | 2.00 | 0.02 | 0.0038 | - | 56 B | NA | +| Method | Categories | Mean | Error | StdDev | Median | Ratio | RatioSD | Gen0 | Gen1 | Allocated | Alloc Ratio | +|-------------------------- |--------------------------- |------------:|----------:|-----------:|------------:|------:|--------:|-------:|-------:|----------:|------------:| +| ReadDoubles | 1,000 double, read | 1,132.50 ns | 9.234 ns | 12.944 ns | 1,137.22 ns | 1.00 | 0.02 | 0.9651 | 0.0267 | 8080 B | 1.00 | +| ReadDoublesUpstream | 1,000 double, read | 3,968.92 ns | 18.600 ns | 27.840 ns | 3,969.66 ns | 3.51 | 0.05 | 0.9613 | 0.0229 | 8080 B | 1.00 | +| | | | | | | | | | | | | +| WriteDoubles | 1,000 double, write | 948.57 ns | 3.318 ns | 4.966 ns | 947.39 ns | 1.00 | 0.01 | - | - | - | NA | +| WriteDoublesUpstream | 1,000 double, write | 2,815.56 ns | 91.288 ns | 127.973 ns | 2,776.58 ns | 2.97 | 0.13 | - | - | - | NA | +| | | | | | | | | | | | | +| ReadInts | 100 int32, read | 116.88 ns | 0.412 ns | 0.617 ns | 117.06 ns | 1.00 | 0.01 | 0.0573 | - | 480 B | 1.00 | +| ReadIntsUpstream | 100 int32, read | 287.48 ns | 2.200 ns | 3.293 ns | 286.73 ns | 2.46 | 0.03 | 0.0572 | - | 480 B | 1.00 | +| | | | | | | | | | | | | +| WriteInts | 100 int32, write | 93.98 ns | 0.841 ns | 1.258 ns | 94.14 ns | 1.00 | 0.02 | - | - | - | NA | +| WriteIntsUpstream | 100 int32, write | 191.20 ns | 0.594 ns | 0.871 ns | 190.90 ns | 2.03 | 0.03 | - | - | - | NA | +| | | | | | | | | | | | | +| ReadItems | 100 structs, read | 1,885.27 ns | 12.637 ns | 18.915 ns | 1,883.79 ns | 1.00 | 0.01 | 1.0605 | 0.0324 | 8880 B | 1.00 | +| ReadItemsUpstream | 100 structs, read | 2,577.09 ns | 13.548 ns | 20.278 ns | 2,578.30 ns | 1.37 | 0.02 | 1.0605 | 0.0305 | 8880 B | 1.00 | +| | | | | | | | | | | | | +| WriteItems | 100 structs, write | 838.08 ns | 7.304 ns | 10.706 ns | 836.80 ns | 1.00 | 0.02 | - | - | - | NA | +| WriteItemsUpstream | 100 structs, write | 1,614.65 ns | 12.419 ns | 17.810 ns | 1,609.31 ns | 1.93 | 0.03 | - | - | - | NA | +| | | | | | | | | | | | | +| ReadBig | 4 KB order, read | 6,068.55 ns | 32.430 ns | 46.510 ns | 6,068.76 ns | 1.00 | 0.01 | 3.0060 | 0.2365 | 25192 B | 1.00 | +| ReadBigUpstream | 4 KB order, read | 9,860.83 ns | 84.786 ns | 126.903 ns | 9,821.58 ns | 1.62 | 0.02 | 2.9755 | 0.2289 | 24920 B | 0.99 | +| | | | | | | | | | | | | +| WriteBig | 4 KB order, write | 2,937.87 ns | 33.570 ns | 49.206 ns | 2,932.67 ns | 1.00 | 0.02 | - | - | - | NA | +| WriteBigUpstream | 4 KB order, write | 5,606.50 ns | 42.249 ns | 63.236 ns | 5,616.70 ns | 1.91 | 0.04 | 0.1831 | - | 1560 B | NA | +| | | | | | | | | | | | | +| ReadOrder | Order, read | 415.80 ns | 8.652 ns | 12.949 ns | 423.71 ns | 1.00 | 0.04 | 0.2904 | 0.0024 | 2432 B | 1.00 | +| ReadOrderUpstream | Order, read | 602.62 ns | 3.316 ns | 4.964 ns | 601.33 ns | 1.45 | 0.05 | 0.2880 | 0.0019 | 2416 B | 0.99 | +| | | | | | | | | | | | | +| WriteOrder | Order, write | 143.38 ns | 0.614 ns | 0.919 ns | 143.51 ns | 1.00 | 0.01 | - | - | - | NA | +| WriteOrderUpstream | Order, write | 438.93 ns | 2.156 ns | 3.227 ns | 438.98 ns | 3.06 | 0.03 | 0.0181 | - | 152 B | NA | +| | | | | | | | | | | | | +| ReadIntDoubleMap | map, read | 378.38 ns | 1.221 ns | 1.790 ns | 378.41 ns | 1.00 | 0.01 | 0.3767 | 0.0043 | 3152 B | 1.00 | +| ReadIntDoubleMapUpstream | map, read | 1,743.88 ns | 7.671 ns | 11.481 ns | 1,741.92 ns | 4.61 | 0.04 | 1.2207 | 0.0191 | 10216 B | 3.24 | +| | | | | | | | | | | | | +| WriteIntDoubleMap | map, write | 174.01 ns | 0.750 ns | 1.122 ns | 174.26 ns | 1.00 | 0.01 | - | - | - | NA | +| WriteIntDoubleMapUpstream | map, write | 789.83 ns | 1.611 ns | 2.205 ns | 790.04 ns | 4.54 | 0.03 | 0.0067 | - | 56 B | NA | +| | | | | | | | | | | | | +| ReadStringMap | map, read | 2,171.90 ns | 10.869 ns | 16.269 ns | 2,172.01 ns | 1.00 | 0.01 | 1.3199 | 0.0496 | 11072 B | 1.00 | +| ReadStringMapUpstream | map, read | 3,346.18 ns | 13.529 ns | 19.403 ns | 3,344.49 ns | 1.54 | 0.01 | 2.1667 | 0.1068 | 18136 B | 1.64 | +| | | | | | | | | | | | | +| WriteStringMap | map, write | 553.39 ns | 1.804 ns | 2.700 ns | 553.02 ns | 1.00 | 0.01 | - | - | - | NA | +| WriteStringMapUpstream | map, write | 1,960.26 ns | 8.383 ns | 12.287 ns | 1,960.77 ns | 3.54 | 0.03 | 0.0038 | - | 56 B | NA |