Skip to content

Reduce saved state in StreamReader async reads - #136801

Merged
jkotas merged 1 commit into
dotnet:mainfrom
asylkhan-azat:streamreader-async-optimization
Nov 2, 2026
Merged

jkotas merged 1 commit into
dotnet:mainfrom
asylkhan-azat:streamreader-async-optimization

Conversation

@asylkhan-azat

@asylkhan-azat asylkhan-azat commented Nov 1, 2026 •

Copy link
Copy Markdown
Contributor

Remove tmpByteBuffer and tmpStream from StreamReader.ReadAsyncInternal and ReadBufferAsync and use the readonly fields directly.

I've also run benchmarks with both runtime async enabled & disabled.

Details
[MemoryDiagnoser]
public class StreamReaderBenchmarks
{
    private string _path = null!;
    private readonly char[] _buffer = new char[4096];

    [GlobalSetup]
    public void Setup()
    {
        _path = Path.GetTempFileName();
        string line = new string('x', 127) + "\n";
        using var writer = new StreamWriter(_path, false, new UTF8Encoding(false));
        for (int i = 0; i < 8192; i++)
            writer.Write(line);
    }

    [GlobalCleanup]
    public void Cleanup() => File.Delete(_path);

    [Benchmark]
    public async Task<int> ReadAsync()
    {
        using var stream = new FileStream(_path, FileMode.Open, FileAccess.Read, FileShare.Read,
            4096, FileOptions.Asynchronous | FileOptions.SequentialScan);
        using var reader = new StreamReader(stream);
        int total = 0, count;
        while ((count = await reader.ReadAsync(_buffer.AsMemory())) != 0)
            total += count;
        return total;
    }

    [Benchmark]
    public async Task<int> ReadLineAsync()
    {
        using var stream = new FileStream(_path, FileMode.Open, FileAccess.Read, FileShare.Read,
            4096, FileOptions.Asynchronous | FileOptions.SequentialScan);
        using var reader = new StreamReader(stream);
        int total = 0;
        while (await reader.ReadLineAsync() is string line)
            total += line.Length;
        return total;
    }
}

Benchamrk results:

Details
BenchmarkDotNet v0.15.8, Windows 11 (10.0.26300.9818)
AMD Ryzen 5 7500F 3.70GHz, 1 CPU, 12 logical and 6 physical cores
.NET SDK 12.0.100-alpha.1.26480.102
  [Host] : .NET 12.0.0 (12.0.0-dev, 42.42.42.42424), X64 RyuJIT x86-64-v4

Job=ShortRun  Toolchain=InProcessEmitToolchain  IterationCount=15  
IterationTime=500ms  LaunchCount=1  WarmupCount=5  

Runtime async, before change

Method Mean Error StdDev Gen0 Gen1 Allocated
ReadAsync 2.775 ms 0.0184 ms 0.0163 ms 10.4167 - 180.99 KB
ReadLineAsync 3.129 ms 0.0217 ms 0.0203 ms 175.0000 6.2500 2928.83 KB

Runtime async, after change

Method Mean Error StdDev Gen0 Gen1 Allocated
ReadAsync 2.782 ms 0.0252 ms 0.0236 ms 5.6818 - 172.94 KB
ReadLineAsync 3.126 ms 0.0511 ms 0.0478 ms 176.1364 5.6818 2924.83 KB

Roslyn, before change

Method Mean Error StdDev Gen0 Gen1 Allocated
ReadAsync 2.994 ms 0.0608 ms 0.0569 ms 5.2083 - 88.52 KB
ReadLineAsync 3.370 ms 0.1088 ms 0.1018 ms 175.0000 6.2500 2878.56 KB

Roslyn, after change

Method Mean Error StdDev Gen0 Gen1 Allocated
ReadAsync 2.956 ms 0.0280 ms 0.0262 ms - - 80.52 KB
ReadLineAsync 3.322 ms 0.1617 ms 0.1513 ms 175.0000 6.2500 2874.54 KB

@dotnet-policy-service dotnet-policy-service Bot added the community-contribution Indicates that the PR has been added by a community member label Nov 1, 2026
@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
Successfully started running 3 pipeline(s).
12 pipeline(s) were filtered out due to trigger conditions.
There may be pipelines that require an authorized user to comment /azp run to run.

@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Tagging subscribers to this area: @dotnet/area-system-io
See info in area-owners.md if you want to be subscribed.

@asylkhan-azat

Copy link
Copy Markdown
Contributor Author

Btw, I've no idea why the runtime async is so much more expensive compared to roslyn

@unsafePtr

Copy link
Copy Markdown
Contributor

This saved state was used for hoisting, so not sure if it's actually a good change

@asylkhan-azat

Copy link
Copy Markdown
Contributor Author

This saved state was used for hoisting, so not sure if it's actually a good change

I guess this hoisting was needed because the object inheirts the MarshalByRefObject where the field accesses where a problem on older versions of NET Framework. I'm not sure if currently it earns it's keep

@jkotas jkotas left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks

@jkotas
jkotas merged commit ec3c6d9 into dotnet:main Nov 2, 2026
144 of 146 checks passed
@asylkhan-azat
asylkhan-azat deleted the streamreader-async-optimization branch November 2, 2026 07:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area-System.IO community-contribution Indicates that the PR has been added by a community member

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants