-
Notifications
You must be signed in to change notification settings - Fork 5.3k
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" ofArray.ConstrainedCopyfor single-dimensional zero-based arrays. - Line 360 is
void ConstrainedCopy(Array, int, Array, int, int), which doesn't include a fast path and just callsCopyImpl. The latter does complex error checking before reaching line 459, which is the "meat" ofConstrainedCopy(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.