Skip to content

Eliminate heap allocations in Random/RandomNumberGenerator polyfills#85

Merged
Tyrrrz merged 5 commits intomasterfrom
copilot/optimize-nextbytes-allocation
Feb 27, 2026
Merged

Eliminate heap allocations in Random/RandomNumberGenerator polyfills#85
Tyrrrz merged 5 commits intomasterfrom
copilot/optimize-nextbytes-allocation

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented Feb 27, 2026

  • Explore repository and understand codebase
  • Fix NetCore21/Random.cs: Reverted ArrayPool approach — use new byte[buffer.Length] since Random.NextBytes(byte[]) has no count overload and would over-generate with a larger rented buffer; removed unused using System.Buffers
  • Fix NetCore21/RandomNumberGenerator.cs: Use ArrayPool<byte> for GetBytes(Span<byte>); cache RandomNumberGeneratorEx singleton — Instance as property
  • Fix NetCore30/RandomNumberGenerator.cs: Cache singleton RNG; use GetBytes(buffer, 0, 4) to avoid over-generating bytes — Instance as property
  • Fix Net60/Random.cs: Use buffer.AsSpan(0, 8) / buffer.AsSpan(0, 4) when calling NextBytes on rented buffers to avoid over-generating bytes
  • Fix Net80/RandomNumberGenerator.cs: Use ArrayPool<char> for GetString; clearArray: true; remove redundant hex.Length == stringLength conditional from Substring
  • Fix Net100/Random.cs: Use ArrayPool<char> for GetString; use ArrayPool<byte> for GetHexString; remove redundant hex.Length == stringLength conditional; add blank line before try
  • Run tests to validate changes (264 pass, 6 unrelated pre-existing failures)
  • Code review (no issues)
Original prompt

Overview

Several polyfill implementations in this repository perform unnecessary heap allocations that can be eliminated using ArrayPool<T> (already available as a polyfill itself), pooled char[] instead of StringBuilder for fixed-length string building, and a cached RandomNumberGenerator singleton. The following files all need changes.


1. PolyShim/NetCore21/Random.csNextBytes(Span<byte>)

Problem: buffer.ToArray() allocates a new byte[] on every call.

public void NextBytes(Span<byte> buffer)
{
    var bufferArray = buffer.ToArray(); // ❌
    random.NextBytes(bufferArray);
    bufferArray.CopyTo(buffer);
}

Fix: Use ArrayPool<byte>:

public void NextBytes(Span<byte> buffer)
{
    if (buffer.IsEmpty)
        return;

    var bufferArray = ArrayPool<byte>.Shared.Rent(buffer.Length);
    try
    {
        random.NextBytes(bufferArray);
        bufferArray.AsSpan(0, buffer.Length).CopyTo(buffer);
    }
    finally
    {
        ArrayPool<byte>.Shared.Return(bufferArray);
    }
}

Also add using System.Buffers; to the file.


2. PolyShim/NetCore21/RandomNumberGenerator.csGetBytes(Span<byte>) and Fill(Span<byte>)

Problem 1: GetBytes(Span<byte>) allocates new byte[data.Length] on every call.

public void GetBytes(Span<byte> data)
{
    if (data.Length == 0)
        return;

    var buffer = new byte[data.Length]; // ❌
    rng.GetBytes(buffer);
    buffer.CopyTo(data);
}

Fix: Use ArrayPool<byte> (with clearArray: true since this is crypto data):

public void GetBytes(Span<byte> data)
{
    if (data.Length == 0)
        return;

    var buffer = ArrayPool<byte>.Shared.Rent(data.Length);
    try
    {
        rng.GetBytes(buffer);
        buffer.AsSpan(0, data.Length).CopyTo(data);
    }
    finally
    {
        ArrayPool<byte>.Shared.Return(buffer, clearArray: true);
    }
}

Problem 2: Fill(Span<byte>) calls RandomNumberGenerator.Create() and Dispose() on every single call — creating and destroying a native OS crypto handle each time.

public static void Fill(Span<byte> data)
{
    if (data.Length == 0)
        return;

    var rng = RandomNumberGenerator.Create(); // ❌ new native handle per call
    try
    {
        rng.GetBytes(data);
    }
    finally
    {
        ((IDisposable)rng).Dispose();
    }
}

Fix: Cache a shared static singleton (thread-safe, as RandomNumberGenerator implementations are thread-safe):

Add a file-scoped helper class before the polyfill class:

file static class RandomNumberGeneratorEx
{
    public static readonly RandomNumberGenerator Instance = RandomNumberGenerator.Create();
}

Then update Fill:

public static void Fill(Span<byte> data)
{
    if (data.Length == 0)
        return;

    RandomNumberGeneratorEx.Instance.GetBytes(data);
}

Also add using System.Buffers; to the file.


3. PolyShim/NetCore30/RandomNumberGenerator.csGetInt32new byte[4] inside rejection-sampling loop + Create() per call

Problem: Both new byte[4] inside the loop AND RandomNumberGenerator.Create() per call:

public static int GetInt32(int fromInclusive, int toExclusive)
{
    var range = (uint)(toExclusive - fromInclusive);
    var rng = RandomNumberGenerator.Create(); // ❌ new native handle per call
    try
    {
        var rejectionThreshold = uint.MaxValue - (uint.MaxValue % range + 1) % range;

        uint result;
        do
        {
            var buffer = new byte[4]; // ❌ allocation inside loop
            rng.GetBytes(buffer);
            result = BitConverter.ToUInt32(buffer, 0);
        } while (result > rejectionThreshold);

        return (int)(result % range) + fromInclusive;
    }
    finally
    {
        ((IDisposable)rng).Dispose();
    }
}

Fix: Cache a singleton RNG, and use ArrayPool<byte> rented outside the loop:

Add a file-scoped helper class:

file static class RandomNumberGeneratorEx
{
    public static readonly RandomNumberGenerator Instance = RandomNumberGenerator.Create();
}

Then update GetInt32:

public static int GetInt32(int fromInclusive, int toExclusive)
{
    var range = (uint)(toExclusive - fromInclusive);
    var rejectionThreshold = uint.MaxValue - (uint.MaxValue % range + 1) % range;

    var buffer = ArrayPool<byte>.Shared.Rent(4);
    try
    {
        uint result;
        do
        {
            RandomNumberGeneratorEx.Instance.GetBytes(buffer);
            result = BitConverter.ToUInt32(buffer, 0);
        } while (result > rejectionThreshold);

        return (int)(result % range) + fromInclusive;
    }
    finally
    {
        ArrayPool<byte>.Shared.Return(buffer, clearArray: true);
    }
}

Also add using System.Buffers; to the file.


4. PolyShim/Net60/Random.csNextInt64 and NextSingle

Problem 1: NextInt64 allocates new byte[8] inside the rejection-s...

This pull request was created from Copilot chat.


💡 You can make Copilot smarter by setting up custom instructions, customizing its development environment and configuring Model Context Protocol (MCP) servers. Learn more Copilot coding agent tips in the docs.

… singleton

Co-authored-by: Tyrrrz <1935960+Tyrrrz@users.noreply.github.com>
Copilot AI changed the title [WIP] Optimize NextBytes method to reduce heap allocations Eliminate heap allocations in Random/RandomNumberGenerator polyfills Feb 27, 2026
@Tyrrrz Tyrrrz added the enhancement New feature or request label Feb 27, 2026
@Tyrrrz Tyrrrz marked this pull request as ready for review February 27, 2026 10:15
Copilot AI review requested due to automatic review settings February 27, 2026 10:15
Copy link
Copy Markdown
Owner

@Tyrrrz Tyrrrz left a comment

Choose a reason for hiding this comment

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

…tting

Co-authored-by: Tyrrrz <1935960+Tyrrrz@users.noreply.github.com>
Copy link
Copy Markdown
Contributor Author

Copilot AI commented Feb 27, 2026

Just as a heads up, I was blocked by some firewall rules while working on your feedback. Expand below for details.

Warning

Firewall rules blocked me from connecting to one or more addresses (expand for details)

I tried to connect to the following addresses, but was blocked by firewall rules:

  • example.com
    • Triggering command: /usr/share/dotnet/dotnet /usr/share/dotnet/dotnet exec --runtimeconfig /home/REDACTED/work/PolyShim/PolyShim/PolyShim.Tests/bin/Debug/net10.0/PolyShim.Tests.runtimeconfig.json --depsfile /home/REDACTED/work/PolyShim/PolyShim/PolyShim.Tests/bin/Debug/net10.0/PolyShim.Tests.deps.json /home/REDACTED/work/PolyShim/PolyShim/PolyShim.Tests/bin/Debug/net10.0/testhost.dll --port 44155 --endpoint 127.0.0.1:044155 --role client --parentprocessid 4359 --telemetryoptedin false (dns block)

If you need me to access, download, or install something from one of these locations, you can either:

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

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 eliminates heap allocations in Random and RandomNumberGenerator polyfill implementations by utilizing ArrayPool<T> for temporary buffers instead of allocating new arrays on every call. The changes also introduce a cached singleton RandomNumberGenerator instance to avoid repeatedly creating and disposing native OS crypto handles, and replace StringBuilder with pooled char arrays for fixed-length string generation.

Changes:

  • Replace heap-allocated byte arrays with ArrayPool<byte> rentals in Random and RandomNumberGenerator polyfills
  • Introduce cached singleton RandomNumberGenerator instance to avoid create/dispose overhead
  • Replace StringBuilder with ArrayPool<char> for fixed-length string generation
  • Optimize substring operations to avoid redundant string copies when output is already correct length
  • Properly clear cryptographic data from pooled buffers using clearArray: true

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
PolyShim/NetCore21/Random.cs Uses ArrayPool for NextBytes(Span) instead of buffer.ToArray()
PolyShim/NetCore21/RandomNumberGenerator.cs Uses ArrayPool for GetBytes(Span) and introduces cached singleton RNG for Fill()
PolyShim/NetCore30/RandomNumberGenerator.cs Uses ArrayPool in GetInt32 rejection loop and cached singleton RNG
PolyShim/Net60/Random.cs Uses ArrayPool for NextInt64 and NextSingle instead of allocating byte arrays in loops
PolyShim/Net80/RandomNumberGenerator.cs Replaces StringBuilder with ArrayPool in GetString and optimizes substring in GetHexString
PolyShim/Net100/Random.cs Uses ArrayPool in GetHexString, replaces StringBuilder with ArrayPool in GetString, and optimizes substring

The changes are well-structured and follow good practices for most parts. However, there is one performance issue in NetCore30/RandomNumberGenerator.cs that should be addressed before merging.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Copy link
Copy Markdown
Owner

@Tyrrrz Tyrrrz left a comment

Choose a reason for hiding this comment

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

var bufferArray = ArrayPool<byte>.Shared.Rent(buffer.Length);
try
{
random.NextBytes(bufferArray);
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

This can populate more than necessary

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in b84a204 — reverted to new byte[buffer.Length] instead of ArrayPool.Rent. Since Random.NextBytes(byte[]) fills the entire array and has no count-limited overload, using a rented buffer (which may be larger) would over-generate. The exact-size allocation avoids this.

…ditionals, formatting

Co-authored-by: Tyrrrz <1935960+Tyrrrz@users.noreply.github.com>
Copy link
Copy Markdown
Contributor Author

Copilot AI commented Feb 27, 2026

Just as a heads up, I was blocked by some firewall rules while working on your feedback. Expand below for details.

Warning

Firewall rules blocked me from connecting to one or more addresses (expand for details)

I tried to connect to the following addresses, but was blocked by firewall rules:

  • example.com
    • Triggering command: /usr/share/dotnet/dotnet /usr/share/dotnet/dotnet exec --runtimeconfig /home/REDACTED/work/PolyShim/PolyShim/PolyShim.Tests/bin/Debug/net10.0/PolyShim.Tests.runtimeconfig.json --depsfile /home/REDACTED/work/PolyShim/PolyShim/PolyShim.Tests/bin/Debug/net10.0/PolyShim.Tests.deps.json /home/REDACTED/work/PolyShim/PolyShim/PolyShim.Tests/bin/Debug/net10.0/testhost.dll --port 45627 --endpoint 127.0.0.1:045627 --role client --parentprocessid 4438 --telemetryoptedin false (dns block)

If you need me to access, download, or install something from one of these locations, you can either:

Copy link
Copy Markdown
Owner

@Tyrrrz Tyrrrz left a comment

Choose a reason for hiding this comment

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

@Tyrrrz Tyrrrz requested a review from Copilot February 27, 2026 10:46
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 6 out of 6 changed files in this pull request and generated no new comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@Tyrrrz Tyrrrz merged commit c0458f2 into master Feb 27, 2026
9 checks passed
@Tyrrrz Tyrrrz deleted the copilot/optimize-nextbytes-allocation branch February 27, 2026 10:58
@codecov
Copy link
Copy Markdown

codecov bot commented Feb 27, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 0.00%. Comparing base (834b221) to head (55e43c1).
⚠️ Report is 1 commits behind head on master.

Additional details and impacted files
@@     Coverage Diff      @@
##   master   #85   +/-   ##
============================
============================

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants