From 835c6d3e51b5c1f67bfb3f5b84a5c9752fa43895 Mon Sep 17 00:00:00 2001 From: David Cantu Date: Thu, 5 Mar 2026 15:06:16 -0600 Subject: [PATCH 1/3] Add MemoryStream benchmarks --- .../libraries/System.IO/MemoryStreamTests.cs | 113 ++++++++++++++++++ 1 file changed, 113 insertions(+) create mode 100644 src/benchmarks/micro/libraries/System.IO/MemoryStreamTests.cs diff --git a/src/benchmarks/micro/libraries/System.IO/MemoryStreamTests.cs b/src/benchmarks/micro/libraries/System.IO/MemoryStreamTests.cs new file mode 100644 index 00000000000..496691a62f8 --- /dev/null +++ b/src/benchmarks/micro/libraries/System.IO/MemoryStreamTests.cs @@ -0,0 +1,113 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using BenchmarkDotNet.Attributes; +using MicroBenchmarks; + +namespace System.IO.Tests +{ + [BenchmarkCategory(Categories.Libraries)] + [MemoryDiagnoser] + public class MemoryStreamTests + { + private MemoryStream _stream; + private byte[] _buffer; + + [Params( + 1, // per-call overhead dominates + 4096, // default StreamReader/BufferedStream buffer size + 65536)] // large bulk transfer + public int Size { get; set; } + + [GlobalSetup] + public void Setup() + { + _buffer = new byte[Size]; + new Random(42).NextBytes(_buffer); + _stream = new MemoryStream(_buffer, writable: true); + } + + [GlobalCleanup] + public void Cleanup() + { + _stream.Dispose(); + } + + [Benchmark] + [MemoryRandomization] + public int ReadByte() + { + MemoryStream s = _stream; + s.Position = 0; + int count = 0; + while (s.ReadByte() != -1) + count++; + return count; + } + + [Benchmark] + [MemoryRandomization] + public int ReadByteArray() + { + MemoryStream s = _stream; + s.Position = 0; + int count = 0; + int n; + while ((n = s.Read(_buffer, 0, _buffer.Length)) > 0) + count += n; + return count; + } + + [Benchmark] + [MemoryRandomization] + public int ReadSpan() + { + MemoryStream s = _stream; + s.Position = 0; + int count = 0; + int n; + while ((n = s.Read(_buffer.AsSpan())) > 0) + count += n; + return count; + } + + [Benchmark] + [MemoryRandomization] + public void WriteByte() + { + MemoryStream s = _stream; + s.Position = 0; + int i = 0; + while (i < Size) + s.WriteByte(_buffer[i++]); + } + + [Benchmark] + [MemoryRandomization] + public void WriteByteArray() + { + MemoryStream s = _stream; + s.Position = 0; + s.Write(_buffer, 0, _buffer.Length); + } + + [Benchmark] + [MemoryRandomization] + public void WriteSpan() + { + MemoryStream s = _stream; + s.Position = 0; + s.Write(_buffer.AsSpan()); + } + + [Benchmark] + [MemoryRandomization] + public void CopyTo() + { + MemoryStream s = _stream; + s.Position = 0; + s.CopyTo(Stream.Null); + } + } +} From 4a524dd3656a7b20e3c88206ab0626ccb128d8d3 Mon Sep 17 00:00:00 2001 From: David Cantu Date: Thu, 5 Mar 2026 15:29:16 -0600 Subject: [PATCH 2/3] Add Async benchmarks --- .../libraries/System.IO/MemoryStreamTests.cs | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/src/benchmarks/micro/libraries/System.IO/MemoryStreamTests.cs b/src/benchmarks/micro/libraries/System.IO/MemoryStreamTests.cs index 496691a62f8..a209f8c3f33 100644 --- a/src/benchmarks/micro/libraries/System.IO/MemoryStreamTests.cs +++ b/src/benchmarks/micro/libraries/System.IO/MemoryStreamTests.cs @@ -2,6 +2,8 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. +using System.Threading; +using System.Threading.Tasks; using BenchmarkDotNet.Attributes; using MicroBenchmarks; @@ -72,6 +74,20 @@ public int ReadSpan() return count; } + [Benchmark] + [BenchmarkCategory(Categories.NoWASM)] + [MemoryRandomization] + public async Task ReadAsyncMemory() + { + MemoryStream s = _stream; + s.Position = 0; + int count = 0; + int n; + while ((n = await s.ReadAsync(_buffer, CancellationToken.None)) > 0) + count += n; + return count; + } + [Benchmark] [MemoryRandomization] public void WriteByte() @@ -101,6 +117,16 @@ public void WriteSpan() s.Write(_buffer.AsSpan()); } + [Benchmark] + [BenchmarkCategory(Categories.NoWASM)] + [MemoryRandomization] + public async Task WriteAsyncMemory() + { + MemoryStream s = _stream; + s.Position = 0; + await s.WriteAsync(_buffer, CancellationToken.None); + } + [Benchmark] [MemoryRandomization] public void CopyTo() @@ -109,5 +135,15 @@ public void CopyTo() s.Position = 0; s.CopyTo(Stream.Null); } + + [Benchmark] + [BenchmarkCategory(Categories.NoWASM)] + [MemoryRandomization] + public async Task CopyToAsync() + { + MemoryStream s = _stream; + s.Position = 0; + await s.CopyToAsync(Stream.Null); + } } } From c4491cc2658f13bed5a1705732883bafc98b3e21 Mon Sep 17 00:00:00 2001 From: David Cantu Date: Thu, 5 Mar 2026 16:17:31 -0600 Subject: [PATCH 3/3] Split benchmarks into two classes: chunked/regular --- .../System.IO/MemoryStreamChunkedTests.cs | 150 ++++++++++++++++++ .../libraries/System.IO/MemoryStreamTests.cs | 92 ++--------- 2 files changed, 162 insertions(+), 80 deletions(-) create mode 100644 src/benchmarks/micro/libraries/System.IO/MemoryStreamChunkedTests.cs diff --git a/src/benchmarks/micro/libraries/System.IO/MemoryStreamChunkedTests.cs b/src/benchmarks/micro/libraries/System.IO/MemoryStreamChunkedTests.cs new file mode 100644 index 00000000000..46ec5b66949 --- /dev/null +++ b/src/benchmarks/micro/libraries/System.IO/MemoryStreamChunkedTests.cs @@ -0,0 +1,150 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Threading; +using System.Threading.Tasks; +using BenchmarkDotNet.Attributes; +using MicroBenchmarks; + +namespace System.IO.Tests +{ + /// + /// Chunked / buffered operations. Fixed 64KB stream, chunk size controls call count. + /// Read/Write variants loop over the stream in ChunkSize increments. + /// CopyTo / CopyToAsync pass ChunkSize as the bufferSize parameter. + /// + [BenchmarkCategory(Categories.Libraries)] + public class MemoryStreamChunkedTests + { + private const int StreamSize = 65536; + + private MemoryStream _stream; + private byte[] _buffer; + + [Params( + 1, // 65536 calls, per-call overhead dominates + 4096)] // 16 calls, default StreamReader/BufferedStream buffer size + public int ChunkSize { get; set; } + + [GlobalSetup] + public void Setup() + { + byte[] backing = new byte[StreamSize]; + new Random(42).NextBytes(backing); + _buffer = new byte[ChunkSize]; + _stream = new MemoryStream(backing, writable: true); + } + + [GlobalCleanup] + public void Cleanup() + { + _stream.Dispose(); + } + + [Benchmark] + [MemoryRandomization] + public int ReadByteArray() + { + MemoryStream s = _stream; + s.Position = 0; + int count = 0; + int n; + while ((n = s.Read(_buffer, 0, _buffer.Length)) > 0) + count += n; + return count; + } + + [Benchmark] + [MemoryRandomization] + public int ReadSpan() + { + MemoryStream s = _stream; + s.Position = 0; + int count = 0; + int n; + while ((n = s.Read(_buffer.AsSpan())) > 0) + count += n; + return count; + } + + [Benchmark] + [BenchmarkCategory(Categories.NoWASM)] + [MemoryRandomization] + public async Task ReadAsyncMemory() + { + MemoryStream s = _stream; + s.Position = 0; + int count = 0; + int n; + while ((n = await s.ReadAsync(_buffer, CancellationToken.None)) > 0) + count += n; + return count; + } + + [Benchmark] + [MemoryRandomization] + public void WriteByteArray() + { + MemoryStream s = _stream; + s.Position = 0; + int remaining = StreamSize; + while (remaining > 0) + { + int chunk = Math.Min(_buffer.Length, remaining); + s.Write(_buffer, 0, chunk); + remaining -= chunk; + } + } + + [Benchmark] + [MemoryRandomization] + public void WriteSpan() + { + MemoryStream s = _stream; + s.Position = 0; + int remaining = StreamSize; + while (remaining > 0) + { + int chunk = Math.Min(_buffer.Length, remaining); + s.Write(_buffer.AsSpan(0, chunk)); + remaining -= chunk; + } + } + + [Benchmark] + [BenchmarkCategory(Categories.NoWASM)] + [MemoryRandomization] + public async Task WriteAsyncMemory() + { + MemoryStream s = _stream; + s.Position = 0; + int remaining = StreamSize; + while (remaining > 0) + { + int chunk = Math.Min(_buffer.Length, remaining); + await s.WriteAsync(_buffer.AsMemory(0, chunk), CancellationToken.None); + remaining -= chunk; + } + } + + [Benchmark] + [MemoryRandomization] + public void CopyToWithBufferSize() + { + MemoryStream s = _stream; + s.Position = 0; + s.CopyTo(Stream.Null, ChunkSize); + } + + [Benchmark] + [BenchmarkCategory(Categories.NoWASM)] + [MemoryRandomization] + public async Task CopyToAsyncWithBufferSize() + { + MemoryStream s = _stream; + s.Position = 0; + await s.CopyToAsync(Stream.Null, ChunkSize); + } + } +} diff --git a/src/benchmarks/micro/libraries/System.IO/MemoryStreamTests.cs b/src/benchmarks/micro/libraries/System.IO/MemoryStreamTests.cs index a209f8c3f33..24db4f653ee 100644 --- a/src/benchmarks/micro/libraries/System.IO/MemoryStreamTests.cs +++ b/src/benchmarks/micro/libraries/System.IO/MemoryStreamTests.cs @@ -2,32 +2,33 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -using System.Threading; using System.Threading.Tasks; using BenchmarkDotNet.Attributes; using MicroBenchmarks; namespace System.IO.Tests { + /// + /// Operations that do not receive a user buffer. Backing size controls iteration count. + /// ReadByte / WriteByte exercise per-byte overhead; CopyTo / CopyToAsync use the + /// default buffer size overload. + /// [BenchmarkCategory(Categories.Libraries)] - [MemoryDiagnoser] public class MemoryStreamTests { private MemoryStream _stream; - private byte[] _buffer; [Params( - 1, // per-call overhead dominates - 4096, // default StreamReader/BufferedStream buffer size - 65536)] // large bulk transfer + 1024, // small stream + 65536)] // large stream public int Size { get; set; } [GlobalSetup] public void Setup() { - _buffer = new byte[Size]; - new Random(42).NextBytes(_buffer); - _stream = new MemoryStream(_buffer, writable: true); + byte[] backing = new byte[Size]; + new Random(42).NextBytes(backing); + _stream = new MemoryStream(backing, writable: true); } [GlobalCleanup] @@ -48,83 +49,14 @@ public int ReadByte() return count; } - [Benchmark] - [MemoryRandomization] - public int ReadByteArray() - { - MemoryStream s = _stream; - s.Position = 0; - int count = 0; - int n; - while ((n = s.Read(_buffer, 0, _buffer.Length)) > 0) - count += n; - return count; - } - - [Benchmark] - [MemoryRandomization] - public int ReadSpan() - { - MemoryStream s = _stream; - s.Position = 0; - int count = 0; - int n; - while ((n = s.Read(_buffer.AsSpan())) > 0) - count += n; - return count; - } - - [Benchmark] - [BenchmarkCategory(Categories.NoWASM)] - [MemoryRandomization] - public async Task ReadAsyncMemory() - { - MemoryStream s = _stream; - s.Position = 0; - int count = 0; - int n; - while ((n = await s.ReadAsync(_buffer, CancellationToken.None)) > 0) - count += n; - return count; - } - [Benchmark] [MemoryRandomization] public void WriteByte() { MemoryStream s = _stream; s.Position = 0; - int i = 0; - while (i < Size) - s.WriteByte(_buffer[i++]); - } - - [Benchmark] - [MemoryRandomization] - public void WriteByteArray() - { - MemoryStream s = _stream; - s.Position = 0; - s.Write(_buffer, 0, _buffer.Length); - } - - [Benchmark] - [MemoryRandomization] - public void WriteSpan() - { - MemoryStream s = _stream; - s.Position = 0; - s.Write(_buffer.AsSpan()); - } - - [Benchmark] - [BenchmarkCategory(Categories.NoWASM)] - [MemoryRandomization] - public async Task WriteAsyncMemory() - { - MemoryStream s = _stream; - s.Position = 0; - await s.WriteAsync(_buffer, CancellationToken.None); + for (int i = 0; i < Size; i++) + s.WriteByte((byte)i); } [Benchmark]