Skip to content

Use GC.AllocateUninitializedArray in File.ReadAllBytes[Async] - #132398

Merged
adamsitnik merged 1 commit into
mainfrom
copilot/update-file-readallbytes-async
Aug 17, 2026
Merged

Use GC.AllocateUninitializedArray in File.ReadAllBytes[Async]#132398
adamsitnik merged 1 commit into
mainfrom
copilot/update-file-readallbytes-async

Conversation

Copilot AI commented Aug 17, 2026

Copy link
Copy Markdown
Contributor

File.ReadAllBytes and File.ReadAllBytesAsync allocate their result buffer with new byte[count], paying for zeroing that the read loop immediately overwrites. Idea by @GrabYourPitchforks.

Changes

  • File.ReadAllBytes and File.InternalReadAllBytesAsync now allocate the result via GC.AllocateUninitializedArray<byte>(count).
int count = (int)fileLength;
byte[] bytes = GC.AllocateUninitializedArray<byte>(count);

Safety: both loops fill the entire buffer and throw EndOfFileException if the file yields fewer bytes than its reported length, so the array is either fully written or discarded with the exception — callers can never observe uninitialized memory. GC.AllocateUninitializedArray falls back to a zeroed allocation below its internal size threshold, so only large reads are affected.

Note: the issue refers to GC.GetUninitializedArray; the actual API is GC.AllocateUninitializedArray<T>.

Benchmarks

BenchmarkDotNet, comparing both allocation strategies in otherwise identical RandomAccess-based read loops (in-process toolchain, ShortRun, Linux x64, .NET 11 preview, noisy CI-class machine):

Method FileSize Mean Ratio
Zeroed 1024 2.735 us 1.00
Uninitialized 1024 2.819 us 1.03
ZeroedAsync 1024 22.016 us 1.00
UninitializedAsync 1024 21.328 us 0.97
Zeroed 100000 41.690 us 1.00
Uninitialized 100000 46.091 us 1.11
ZeroedAsync 100000 91.507 us 1.00
UninitializedAsync 100000 93.875 us 1.03
Zeroed 10000000 1,579.992 us 1.00
Uninitialized 10000000 1,359.842 us 0.86
ZeroedAsync 10000000 1,816.723 us 1.00
UninitializedAsync 10000000 1,388.709 us 0.76

10 MB reads are ~14% (sync) / ~24% (async) faster; smaller sizes fall within noise.

Note

This pull request was created by GitHub Copilot.

Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com>
@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
Successfully started running 3 pipeline(s).
13 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.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This pull request optimizes System.IO.File.ReadAllBytes and File.ReadAllBytesAsync in System.Private.CoreLib by avoiding the cost of zero-initializing the destination buffer when the subsequent read loop overwrites the entire array.

Changes:

  • Switch ReadAllBytes to allocate its result buffer via GC.AllocateUninitializedArray<byte>(count) instead of new byte[count].
  • Switch the async implementation path (InternalReadAllBytesAsync) to allocate via GC.AllocateUninitializedArray<byte>(count) as well.
  • Add inline comments documenting why skipping zero-init is safe due to full-buffer overwrite and EOF exception behavior.

@jkotas jkotas added the tenet-performance Performance related issue label Aug 17, 2026
@adamsitnik

Copy link
Copy Markdown
Member

@EgorBot -osx_arm64 -linux_x64 -windows_x64

using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;

BenchmarkSwitcher.FromAssembly(typeof(Benchmarks).Assembly).Run(args);

[MemoryDiagnoser]
public class Benchmarks
{
    private string? _filePath;

    [Params(1024, 100_000, 10_000_000)]
    public int FileSize;

    [GlobalSetup]
    public void Setup()
    {
        _filePath = Path.GetTempFileName();
        byte[] content = new byte[FileSize];
        new Random(42).NextBytes(content);
        File.WriteAllBytes(_filePath, content);
    }

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

    [Benchmark]
    public byte[] Sync() => File.ReadAllBytes(_filePath!);

    [Benchmark]
    public Task<byte[]> Async() => File.ReadAllBytesAsync(_filePath!);
}

@azure-pipelines

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

Comment thread src/libraries/System.Private.CoreLib/src/System/IO/File.cs

@adamsitnik adamsitnik 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.

LGTM. The benchmark results show a 3-6% performance improvement on Unix (macOS and Linux) for medium and large files. For small files, and on Windows in general, the difference is within the margin of error.

Details

Results for macos26_helix_arm64

@adamsitnik

BenchmarkDotNet v0.16.0-preview.1, macOS Sequoia 15.4.1 (24E263) [Darwin 24.4.0]
Apple M4, 1 CPU, 10 logical and 10 physical cores
Memory: 16 GB Total, 4.54 GB Available
.NET SDK 11.0.100-rc.1.26413.103
  [Host]     : .NET 11.0.0 (11.0.0-rc.1.26413.103, 11.0.26.41403), Arm64 RyuJIT armv8.0-a
Method Toolchain FileSize Mean Error Ratio Gen0 Gen1 Gen2 Allocated Alloc Ratio
Sync main** 1024 7.471 μs 0.0381 μs 1.01 0.1297 - - 1.08 KB 1.00
Sync PR #132398 1024 7.408 μs 0.0276 μs 1.00 0.1297 - - 1.08 KB 1.00
Async main 1024 9.767 μs 0.0385 μs 1.00 0.1678 - - 1.38 KB 1.00
Async PR #132398 1024 9.731 μs 0.0509 μs 1.00 0.1678 - - 1.38 KB 1.00
Sync main** 100000 17.348 μs 0.0929 μs 1.01 31.2195 31.2195 31.2195 97.76 KB 1.00
Sync PR #132398 100000 17.158 μs 0.1232 μs 1.00 31.2195 31.2195 31.2195 97.76 KB 1.00
Async main 100000 16.556 μs 0.0899 μs 1.04 31.2195 31.2195 31.2195 98.06 KB 1.00
Async PR #132398 100000 15.899 μs 0.1585 μs 1.00 31.2195 31.2195 31.2195 98.06 KB 1.00
Sync main** 10000000 733.256 μs 6.7370 μs 1.03 166.9922 166.9922 166.9922 9767.1 KB 1.00
Sync PR #132398 10000000 713.734 μs 6.2004 μs 1.00 224.6094 224.6094 224.6094 9767.61 KB 1.00
Async main 10000000 667.484 μs 3.6360 μs 1.16 150.3906 150.3906 150.3906 9767.01 KB 1.00
Async PR #132398 10000000 575.771 μs 7.2609 μs 1.00 109.3750 109.3750 109.3750 9766.69 KB 1.00

Full logs

Results for ubuntu24_azure_turin

@adamsitnik

BenchmarkDotNet v0.16.0-preview.1, Linux Ubuntu 24.04.4 LTS (Noble Numbat)
AMD EPYC 9V45 2.60GHz, 1 CPU, 8 logical and 4 physical cores
Memory: 31.34 GB Total, 10.28 GB Available
.NET SDK 11.0.100-rc.1.26413.103
  [Host]     : .NET 11.0.0 (11.0.0-rc.1.26413.103, 11.0.26.41403), X64 RyuJIT x86-64-v4
Method Toolchain FileSize Mean Error Ratio Gen0 Gen1 Gen2 Allocated Alloc Ratio
Sync main** 1024 5.531 μs 0.0499 μs 1.01 0.0610 - - 1.08 KB 1.00
Sync PR #132398 1024 5.458 μs 0.0456 μs 1.00 0.0610 - - 1.08 KB 1.00
Async main 1024 7.303 μs 0.1409 μs 0.99 0.0763 - - 1.37 KB 1.00
Async PR #132398 1024 7.349 μs 0.1433 μs 1.00 0.0839 - - 1.38 KB 1.00
Sync main** 100000 44.881 μs 0.3425 μs 0.98 31.1890 31.1890 31.1890 97.76 KB 1.00
Sync PR #132398 100000 45.630 μs 0.5886 μs 1.00 31.1890 31.1890 31.1890 97.76 KB 1.00
Async main 100000 12.532 μs 0.2492 μs 1.06 31.2347 31.2347 31.2347 98.06 KB 1.00
Async PR #132398 100000 11.877 μs 0.2354 μs 1.00 31.2347 31.2347 31.2347 98.06 KB 1.00
Sync main** 10000000 608.650 μs 5.2426 μs 1.06 500.0000 500.0000 500.0000 9766.11 KB 1.00
Sync PR #132398 10000000 571.953 μs 6.8419 μs 1.00 500.0000 500.0000 500.0000 9766.11 KB 1.00
Async main 10000000 1,144.992 μs 4.6155 μs 1.06 396.4844 396.4844 396.4844 9767.52 KB 1.00
Async PR #132398 10000000 1,078.073 μs 21.3568 μs 1.00 396.4844 396.4844 396.4844 9769.31 KB 1.00

Full logs

Results for windows_azure_turin

@adamsitnik


BenchmarkDotNet v0.16.0-preview.1, Windows 11 (10.0.26100.33296/24H2/2024Update/HudsonValley) (Hyper-V)
AMD EPYC 9V45 2.60GHz, 1 CPU, 8 logical and 4 physical cores
Memory: 31.99 GB Total, 27.01 GB Available
.NET SDK 11.0.100-rc.1.26413.103
  [Host]     : .NET 11.0.0 (11.0.0-rc.1.26413.103, 11.0.26.41403), X64 RyuJIT x86-64-v4


Method Toolchain FileSize Mean Error Ratio Gen0 Gen1 Gen2 Allocated Alloc Ratio
Sync main** 1024 22.55 μs 0.364 μs 1.02 0.0610 - - 1.1 KB 1.00
Sync PR #132398 1024 22.12 μs 0.315 μs 1.00 0.0610 - - 1.1 KB 1.00
Async main 1024 35.10 μs 0.669 μs 1.01 0.0610 - - 1.61 KB 1.00
Async PR #132398 1024 34.66 μs 0.456 μs 1.00 0.0610 - - 1.61 KB 1.00
Sync main** 100000 57.16 μs 0.689 μs 1.12 31.1890 31.1890 31.1890 97.78 KB 1.00
Sync PR #132398 100000 51.09 μs 0.581 μs 1.00 31.1890 31.1890 31.1890 97.78 KB 1.00
Async main 100000 39.72 μs 0.400 μs 1.02 31.1890 31.1890 31.1890 98.29 KB 1.00
Async PR #132398 100000 38.92 μs 0.759 μs 1.00 31.1890 31.1890 31.1890 98.29 KB 1.00
Sync main** 10000000 1,701.06 μs 33.238 μs 1.00 500.0000 500.0000 500.0000 9766.13 KB 1.00
Sync PR #132398 10000000 1,699.38 μs 33.987 μs 1.00 500.0000 500.0000 500.0000 9766.13 KB 1.00
Async main 10000000 2,419.92 μs 79.170 μs 1.01 390.6250 390.6250 390.6250 9768 KB 1.00
Async PR #132398 10000000 2,395.01 μs 47.691 μs 1.00 390.6250 390.6250 390.6250 9769.52 KB 1.00

Full logs

@adamsitnik

Copy link
Copy Markdown
Member

/ba-g networking failures, unrelated

@adamsitnik
adamsitnik merged commit 185098c into main Aug 17, 2026
134 of 139 checks passed
@adamsitnik
adamsitnik deleted the copilot/update-file-readallbytes-async branch August 17, 2026 16:21
@adamsitnik adamsitnik added this to the 11.0.0 milestone Aug 17, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area-System.IO tenet-performance Performance related issue

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants