Tools
Tools: .NET: CopyToAsync vs WriteAsync: The Benchmark You Didn’t Know You Needed
2026-03-02
0 views
admin
The Benchmark ## The Code ## The Results ## The Real Lesson I had a task where one small part involved writing a byte array to a MemoryStream. Since this was a trivial task, I let Copilot generate the code. But Copilot used CopyToAsync instead of WriteAsync. That raised a question, so I asked why. It kept trying to convince me that CopyToAsync was a better fit for this scenario (a 1 MB file), and I quote: CopyToAsync might offer better performance due to its internal optimizations for copying data between streams. The performance difference between these two approaches can be minimal for small to moderately sized data. According to Copilot: But here’s the important detail: I wasn’t copying between two independent streams.
I already had a byte array in memory. In that case, wrapping it in a MemoryStream just to call CopyToAsync adds an extra abstraction layer. So I was skeptical. Copilot was confident.
I was suspicious. As a responsible developer, I couldn’t sleep until I proved my point. So I wrote a benchmark. I tested four methods: The benchmark uses BenchmarkDotNet and tests copying from a byte array into a MemoryStream. The difference? Statistically negligible for both mean execution time and allocations.
Write avoids creating the extra MemoryStream wrapper, but the practical impact remains minimal even at 50 MB. This doesn’t mean CopyTo is bad. It’s the correct API to use when copying between two arbitrary streams. But it is not “magically faster.” In fact, when your source is already a byte array, Write is: Yeah, I put all this effort into investigating a single line of abstraction suggested by Copilot and even wrote a post about it. But this wasn’t about proving Copilot wrong, it was about something more important: AI suggestions are hypotheses, not conclusions. Measure when it matters. Trust evidence over assumptions. If you already have the buffer, use the buffer API. The right choice depends on the scenario, not on AI suggestions about “internal optimizations.” And yes… now I can sleep. Templates let you quickly answer FAQs or store snippets for re-use. Are you sure you want to hide this comment? It will become hidden in your post, but will still be visible via the comment's permalink. Hide child comments as well For further actions, you may consider blocking this person and/or reporting abuse CODE_BLOCK:
BenchmarkDotNet v0.15.8, Windows 10 (10.0.19045.6466/22H2/2022Update)
Intel Core i5-6400 CPU 2.70GHz (Skylake), 1 CPU, 4 logical and 4 physical cores
.NET SDK 10.0.102 [Host] : .NET 10.0.2 (10.0.2, 10.0.225.61305), X64 RyuJIT x86-64-v3 .NET 10.0 : .NET 10.0.2 (10.0.2, 10.0.225.61305), X64 RyuJIT x86-64-v3 Job=.NET 10.0 Runtime=.NET 10.0 Enter fullscreen mode Exit fullscreen mode CODE_BLOCK:
BenchmarkDotNet v0.15.8, Windows 10 (10.0.19045.6466/22H2/2022Update)
Intel Core i5-6400 CPU 2.70GHz (Skylake), 1 CPU, 4 logical and 4 physical cores
.NET SDK 10.0.102 [Host] : .NET 10.0.2 (10.0.2, 10.0.225.61305), X64 RyuJIT x86-64-v3 .NET 10.0 : .NET 10.0.2 (10.0.2, 10.0.225.61305), X64 RyuJIT x86-64-v3 Job=.NET 10.0 Runtime=.NET 10.0 CODE_BLOCK:
BenchmarkDotNet v0.15.8, Windows 10 (10.0.19045.6466/22H2/2022Update)
Intel Core i5-6400 CPU 2.70GHz (Skylake), 1 CPU, 4 logical and 4 physical cores
.NET SDK 10.0.102 [Host] : .NET 10.0.2 (10.0.2, 10.0.225.61305), X64 RyuJIT x86-64-v3 .NET 10.0 : .NET 10.0.2 (10.0.2, 10.0.225.61305), X64 RyuJIT x86-64-v3 Job=.NET 10.0 Runtime=.NET 10.0 COMMAND_BLOCK:
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Jobs; namespace GeneralBenchmark.CopyToAndWrite
{ [SimpleJob(RuntimeMoniker.Net90)] [Orderer(BenchmarkDotNet.Order.SummaryOrderPolicy.Declared)] [MemoryDiagnoser] public class CopyToAndWriteBenchmark { public IEnumerable<CopyToAndWriteBenchmarkData> Data() { yield return new CopyToAndWriteBenchmarkData(1024 * 12); yield return new CopyToAndWriteBenchmarkData(1024 * 32); yield return new CopyToAndWriteBenchmarkData(1024 * 1024 * 5); yield return new CopyToAndWriteBenchmarkData(1024 * 1024 * 10); yield return new CopyToAndWriteBenchmarkData(1024 * 1024 * 25); yield return new CopyToAndWriteBenchmarkData(1024 * 1024 * 50); } [Benchmark] [ArgumentsSource(nameof(Data))] public async Task MemoryStream_CopyTo_Async(CopyToAndWriteBenchmarkData data) { using var outerStream = new MemoryStream(); using var innerStream = new MemoryStream(data.ByteData); await innerStream.CopyToAsync(outerStream); } [Benchmark] [ArgumentsSource(nameof(Data))] public async Task ByteArray_Write_Async(CopyToAndWriteBenchmarkData data) { using var outerStream = new MemoryStream(); await outerStream.WriteAsync(data.ByteData); } [Benchmark] [ArgumentsSource(nameof(Data))] public void MemoryStream_CopyTo(CopyToAndWriteBenchmarkData data) { using var outerStream = new MemoryStream(); using var innerStream = new MemoryStream(data.ByteData); innerStream.CopyTo(outerStream); } [Benchmark] [ArgumentsSource(nameof(Data))] public void ByteArray_Write(CopyToAndWriteBenchmarkData data) { using var outerStream = new MemoryStream(); outerStream.Write(data.ByteData); } }
} Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK:
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Jobs; namespace GeneralBenchmark.CopyToAndWrite
{ [SimpleJob(RuntimeMoniker.Net90)] [Orderer(BenchmarkDotNet.Order.SummaryOrderPolicy.Declared)] [MemoryDiagnoser] public class CopyToAndWriteBenchmark { public IEnumerable<CopyToAndWriteBenchmarkData> Data() { yield return new CopyToAndWriteBenchmarkData(1024 * 12); yield return new CopyToAndWriteBenchmarkData(1024 * 32); yield return new CopyToAndWriteBenchmarkData(1024 * 1024 * 5); yield return new CopyToAndWriteBenchmarkData(1024 * 1024 * 10); yield return new CopyToAndWriteBenchmarkData(1024 * 1024 * 25); yield return new CopyToAndWriteBenchmarkData(1024 * 1024 * 50); } [Benchmark] [ArgumentsSource(nameof(Data))] public async Task MemoryStream_CopyTo_Async(CopyToAndWriteBenchmarkData data) { using var outerStream = new MemoryStream(); using var innerStream = new MemoryStream(data.ByteData); await innerStream.CopyToAsync(outerStream); } [Benchmark] [ArgumentsSource(nameof(Data))] public async Task ByteArray_Write_Async(CopyToAndWriteBenchmarkData data) { using var outerStream = new MemoryStream(); await outerStream.WriteAsync(data.ByteData); } [Benchmark] [ArgumentsSource(nameof(Data))] public void MemoryStream_CopyTo(CopyToAndWriteBenchmarkData data) { using var outerStream = new MemoryStream(); using var innerStream = new MemoryStream(data.ByteData); innerStream.CopyTo(outerStream); } [Benchmark] [ArgumentsSource(nameof(Data))] public void ByteArray_Write(CopyToAndWriteBenchmarkData data) { using var outerStream = new MemoryStream(); outerStream.Write(data.ByteData); } }
} COMMAND_BLOCK:
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Jobs; namespace GeneralBenchmark.CopyToAndWrite
{ [SimpleJob(RuntimeMoniker.Net90)] [Orderer(BenchmarkDotNet.Order.SummaryOrderPolicy.Declared)] [MemoryDiagnoser] public class CopyToAndWriteBenchmark { public IEnumerable<CopyToAndWriteBenchmarkData> Data() { yield return new CopyToAndWriteBenchmarkData(1024 * 12); yield return new CopyToAndWriteBenchmarkData(1024 * 32); yield return new CopyToAndWriteBenchmarkData(1024 * 1024 * 5); yield return new CopyToAndWriteBenchmarkData(1024 * 1024 * 10); yield return new CopyToAndWriteBenchmarkData(1024 * 1024 * 25); yield return new CopyToAndWriteBenchmarkData(1024 * 1024 * 50); } [Benchmark] [ArgumentsSource(nameof(Data))] public async Task MemoryStream_CopyTo_Async(CopyToAndWriteBenchmarkData data) { using var outerStream = new MemoryStream(); using var innerStream = new MemoryStream(data.ByteData); await innerStream.CopyToAsync(outerStream); } [Benchmark] [ArgumentsSource(nameof(Data))] public async Task ByteArray_Write_Async(CopyToAndWriteBenchmarkData data) { using var outerStream = new MemoryStream(); await outerStream.WriteAsync(data.ByteData); } [Benchmark] [ArgumentsSource(nameof(Data))] public void MemoryStream_CopyTo(CopyToAndWriteBenchmarkData data) { using var outerStream = new MemoryStream(); using var innerStream = new MemoryStream(data.ByteData); innerStream.CopyTo(outerStream); } [Benchmark] [ArgumentsSource(nameof(Data))] public void ByteArray_Write(CopyToAndWriteBenchmarkData data) { using var outerStream = new MemoryStream(); outerStream.Write(data.ByteData); } }
} - Small to medium data: 12 KB – 32 KB
- Large data: 5 MB – 10 MB - MemoryStream.CopyToAsync
- MemoryStream.CopyTo
- MemoryStream.WriteAsync
- MemoryStream.Write - More direct
- Semantically correct
how-totutorialguidedev.toai