Skip to content

Conversation

Copy link
Contributor

Copilot AI commented Dec 14, 2025

Adds a fast path optimization to Array.ConstrainedCopy for single-dimensional zero-based arrays, bringing it to performance parity with Array.Copy. Prior to this change, ConstrainedCopy was ~5x slower than Copy in .NET 9/10 due to the lack of a fast path.

Changes Made

CoreCLR Implementation

  • Created a private helper method: Copy(Array sourceArray, int sourceIndex, Array destinationArray, int destinationIndex, int length, bool reliable) that contains the fast path logic for single-dimensional zero-based arrays
  • Simplified ConstrainedCopy: Now delegates to the private helper with reliable: true for strong exception guarantee
  • Simplified public Copy: The public Copy(Array, int, Array, int, int) method now delegates to the private helper with reliable: false
  • Fast path optimization: The private helper checks if arrays are non-null, same type, single-dimensional zero-based (SZArray), and have valid bounds, then uses Buffer.BulkMoveWithWriteBarrier or SpanHelpers.Memmove for direct copy
  • Code deduplication: Eliminated ~29 lines of duplicated code between ConstrainedCopy and Copy

Mono Implementation

  • Added Mono-specific ConstrainedCopy: Implementation in src/mono/System.Private.CoreLib/src/System/Array.Mono.cs that calls CopyImpl with reliable: true

Performance Impact

Benchmark results confirm expected improvement, bringing Array.ConstrainedCopy to performance parity with Array.Copy for single-dimensional arrays while maintaining the strong exception guarantee (atomicity).

Testing

  • ✅ All 69,527 existing tests in System.Runtime.Tests pass
  • ✅ Build succeeds with no warnings or errors
  • ✅ CodeQL security check passes
  • ✅ No new tests required as existing ConstrainedCopy tests cover the functionality

The fast path preserves the same behavior and strong exception guarantee as before, while the refactored implementation maintains a single source of truth for the optimization logic.

Original prompt

This section details on the original issue you should resolve

<issue_title>Array.ConstrainedCopy doesn't enjoy quick path treatment as Array.Copy</issue_title>
<issue_description>### Description

Recent refactoring of Array.Copy overloads and Array.ConstrainedCopy has unfairly penalized the callers of Array.ConstrainedCopy by not providing a fast path for the latter but providing one for the former.

using System;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;

namespace IsConstrainedCopySlower
{
  public class Program
  {
    // Intentionally small to measure the overhead before starting the actual work.
    private readonly int[] src = new int[1];
    private readonly int[] dst = new int[1];

    [Benchmark]
    public void ArrayCopy() { Array.Copy(src, 0, dst, 0, 1); }

    [Benchmark]
    public void ArrayConstrainedCopy() { Array.ConstrainedCopy(src, 0, dst, 0, 1); }

    public static void Main() { BenchmarkRunner.Run<Program>(); }
  }
}
/*

.NET 10, Windows 11 (24H2) x64.

| Method               | Mean      | Error     | StdDev    |
|--------------------- |----------:|----------:|----------:|
| ArrayCopy            | 0.6524 ns | 0.0042 ns | 0.0040 ns |
| ArrayConstrainedCopy | 3.5042 ns | 0.0012 ns | 0.0011 ns |

.NET 9, Windows 11 (24H2) x64.

| Method               | Mean     | Error     | StdDev    |
|--------------------- |---------:|----------:|----------:|
| ArrayCopy            | 3.749 ns | 0.0030 ns | 0.0027 ns |
| ArrayConstrainedCopy | 5.371 ns | 0.1229 ns | 0.2750 ns |

.NET Framework 4.8.1, Windows 11 (24H2) x64.

| Method               | Mean     | Error     | StdDev    |
|--------------------- |---------:|----------:|----------:|
| ArrayCopy            | 6.879 ns | 0.1550 ns | 0.3592 ns |
| ArrayConstrainedCopy | 6.963 ns | 0.1554 ns | 0.3928 ns |

*/

Notice how in .NET Framework (and earlier versions of .NET I think, but I didn't have the patience to run them), the overheads for Array.Copy and Array.ConstrainedCopy are the same (the difference is explained by fluctuations), but in .NET 9/10, the overhead of Array.ConstrainedCopy is much higher than that of Array.Copy with high confidence.

Analysis

I discovered this change when I was casually browsing the code base, and was wondering whether under=optimization would hurt callers of ConstrainedCopy, and verified.

In Array.cs:

  • Line 399 is the overload void Copy(Array, int, Array, int, int), which includes a fast path (big if) and calls the worker (essentially, this is the "meat" of Array.ConstrainedCopy for single-dimensional zero-based arrays.
  • Line 360 is void ConstrainedCopy(Array, int, Array, int, int), which doesn't include a fast path and just calls CopyImpl. The latter does complex error checking before reaching line 459, which is the "meat" of ConstrainedCopy (but it also handles multi-dimensional arrays).

The suggested solution is to add a fast path in ConstrainedCopy for single-dimensional zero-based arrays like Copy.</issue_description>

<agent_instructions>Please add fast path to Array.ConstrainedCopy - see issue description for details</agent_instructions>

Comments on the Issue (you are @copilot in this section)


💬 We'd love your input! Share your thoughts on Copilot coding agent in our 2 minute survey.

Copilot AI changed the title [WIP] Fix performance issue in Array.ConstrainedCopy Add fast path to Array.ConstrainedCopy for single-dimensional arrays Dec 14, 2025
Copilot AI requested a review from jkotas December 14, 2025 04:26
@jkotas jkotas added area-System.Runtime tenet-performance Performance related issue labels Dec 14, 2025
@dotnet-policy-service
Copy link
Contributor

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

Copilot AI requested a review from jkotas December 14, 2025 05:03
@jkotas
Copy link
Member

jkotas commented Dec 14, 2025

@EgorBot -amd

using System;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;

namespace IsConstrainedCopySlower
{
  public class Program
  {
    // Intentionally small to measure the overhead before starting the actual work.
    private readonly int[] src = new int[1];
    private readonly int[] dst = new int[1];

    [Benchmark]
    public void ArrayCopy() { Array.Copy(src, 0, dst, 0, 1); }

    [Benchmark]
    public void ArrayConstrainedCopy() { Array.ConstrainedCopy(src, 0, dst, 0, 1); }
  }
}

@jkotas jkotas marked this pull request as ready for review December 14, 2025 05:26
Copilot AI review requested due to automatic review settings December 14, 2025 05:26
Copy link
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 PR adds a fast path optimization to Array.ConstrainedCopy for single-dimensional zero-based arrays, bringing it to performance parity with Array.Copy. Prior to this change, ConstrainedCopy was significantly slower than Copy in .NET 9/10 due to the lack of a fast path that skips complex validation for simple array scenarios.

Key Changes:

  • Added inline fast path validation and copy logic for single-dimensional arrays in ConstrainedCopy matching the existing pattern in Array.Copy
  • Added Mono-specific implementation of ConstrainedCopy that delegates to CopyImpl with the reliability flag

Reviewed changes

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

File Description
src/libraries/System.Private.CoreLib/src/System/Array.cs Adds fast path to ConstrainedCopy for single-dimensional zero-based arrays using unsafe pointer operations, matching the existing fast path pattern in Array.Copy
src/mono/System.Private.CoreLib/src/System/Array.Mono.cs Adds Mono-specific ConstrainedCopy implementation that delegates to CopyImpl with reliable: true

The implementation correctly mirrors the fast path logic from Array.Copy (lines 424-452), which is appropriate since the only semantic difference between the two methods is the reliability guarantee when falling back to the slow path. The fast path handles:

  • Null checks for both arrays
  • Type compatibility verification using MethodTable
  • Single-dimensional array detection
  • Bounds validation using unsigned arithmetic overflow protection
  • Appropriate copy operation based on whether the array contains GC pointers

No issues found. The code is well-implemented, follows existing patterns, and should achieve the performance goals stated in the PR description.

@jkotas
Copy link
Member

jkotas commented Dec 14, 2025

EgorBot/runtime-utils#565 (comment) confirmed expected improvement.

@jkotas
Copy link
Member

jkotas commented Dec 15, 2025

@EgorBot -windows_x64 -intel -arm

using System;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;

namespace IsConstrainedCopySlower
{
  public class Program
  {
    // Intentionally small to measure the overhead before starting the actual work.
    private readonly int[] src = new int[1];
    private readonly int[] dst = new int[1];

    [Benchmark]
    public void ArrayCopy() { Array.Copy(src, 0, dst, 0, 1); }
  }
}

@jkotas
Copy link
Member

jkotas commented Dec 16, 2025

@EgorBot -windows_x64

using System;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;

namespace IsConstrainedCopySlower
{
  public class Program
  {
    // Intentionally small to measure the overhead before starting the actual work.
    private readonly int[] src = new int[1];
    private readonly int[] dst = new int[1];

    [Benchmark]
    public void ArrayCopy() { Array.Copy(src, 0, dst, 0, 1); }
  }
}

@jkotas
Copy link
Member

jkotas commented Dec 16, 2025

@EgorBot -windows_intel --envvars DOTNET_ReadyToRun:0 DOTNET_TieredCompilation:0

using System;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;

namespace IsConstrainedCopySlower
{
  public class Program
  {
    // Intentionally small to measure the overhead before starting the actual work.
    private readonly int[] src = new int[1];
    private readonly int[] dst = new int[1];

    [Benchmark]
    public void ArrayCopy() { Array.Copy(src, 0, dst, 0, 1); }
  }
}

@jkotas jkotas merged commit e994f00 into main Dec 17, 2025
143 of 147 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Array.ConstrainedCopy doesn't enjoy quick path treatment as Array.Copy

4 participants