本文來對比多個不同的方法進行數組拷貝,和測試其性能
測試性能必須采用基准(標准)性能測試方法,否則測試結果不可信。在 dotnet 里面,可以采用 BenchmarkDotNet 進行性能測試。詳細請看 C# 標准性能測試
拷貝某個數組的從某個起始點加上某個長度的數據到另一個數組里面,可選方法有很多,本文僅列舉出使用 for
循環拷貝,和使用 Array.Copy 方法和用 Span 方法進行拷貝進行對比
假定有需要被拷貝的數組是 TestData
其定義如下
static Program()
{
TestData = new int[1000];
for (int i = 0; i < 1000; i++)
{
TestData[i] = i;
}
}
private static readonly int[] TestData;
使用 for
循環拷貝的方法如下
public object CopyByFor(int start, int length)
{
var rawPacketData = TestData;
var data = new int[length];
for (int localIndex = 0, rawArrayIndex = start; localIndex < data.Length; localIndex++, rawArrayIndex++)
{
data[localIndex] = rawPacketData[rawArrayIndex];
}
return data;
}
以上代碼返回 data 作為 object 僅僅只是為了做性能測試,避免被 dotnet 優化掉
另一個拷貝數組是采用 Array.Copy
拷貝,邏輯如下
public object CopyByArray(int start, int length)
{
var rawPacketData = TestData;
var data = new int[length];
Array.Copy(rawPacketData,start,data,0, length);
return data;
}
采用新的 dotnet 提供的 Span 進行拷貝,代碼如下
public object CopyBySpan(int start, int length)
{
var rawPacketData = TestData;
var rawArrayStartIndex = start;
var data = rawPacketData.AsSpan(rawArrayStartIndex, length).ToArray();
return data;
}
接着加上一些性能調試輔助邏輯
[Benchmark]
[ArgumentsSource(nameof(ProvideArguments))]
public object CopyByFor(int start, int length)
{
var rawPacketData = TestData;
var data = new int[length];
for (int localIndex = 0, rawArrayIndex = start; localIndex < data.Length; localIndex++, rawArrayIndex++)
{
data[localIndex] = rawPacketData[rawArrayIndex];
}
return data;
}
[Benchmark]
[ArgumentsSource(nameof(ProvideArguments))]
public object CopyByArray(int start, int length)
{
var rawPacketData = TestData;
var data = new int[length];
Array.Copy(rawPacketData,start,data,0, length);
return data;
}
public IEnumerable<object[]> ProvideArguments()
{
foreach (var start in new[] { 0, 10, 100 })
{
foreach (var length in new[] { 10, 20, 100 })
{
yield return new object[] { start, length };
}
}
}
在我的設備上的測試效果如下
BenchmarkDotNet=v0.13.1, OS=Windows 10.0.19042.1200 (20H2/October2020Update)
Intel Core i7-9700K CPU 3.60GHz (Coffee Lake), 1 CPU, 8 logical and 8 physical cores
.NET SDK=6.0.100-preview.7.21379.14
[Host] : .NET 6.0.0 (6.0.21.37719), X64 RyuJIT
DefaultJob : .NET 6.0.0 (6.0.21.37719), X64 RyuJIT
可以看到,在對比使用 for
循環拷貝和使用 Array.Copy
拷貝中,使用 Array.Copy
拷貝的性能更好,在拷貝的數組長度越長的時候,使用 Array.Copy 拷貝性能優勢就更好
接下來再加上 Span 的性能比較,如下面代碼
[Benchmark]
[ArgumentsSource(nameof(ProvideArguments))]
public object CopyBySpan(int start, int length)
{
var rawPacketData = TestData;
var rawArrayStartIndex = start;
var data = rawPacketData.AsSpan(rawArrayStartIndex, length).ToArray();
return data;
}
性能對比測試如下
可以看到 Span 的性能比 Array.Copy
拷貝性能更強
在 Span 里面,轉換為數組的邏輯如下
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public T[] ToArray()
{
if (_length == 0)
return Array.Empty<t>();
var destination = new T[_length];
Buffer.Memmove(ref MemoryMarshal.GetArrayDataReference(destination), ref _pointer.Value, (nuint)_length);
return destination;
}
這里使用到的 Buffer 的有黑科技的 Memmove 方法,此方法的實現如下
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal static void Memmove<t>(ref T destination, ref T source, nuint elementCount)
{
if (!RuntimeHelpers.IsReferenceOrContainsReferences<t>())
{
// Blittable memmove
Memmove(
ref Unsafe.As<t, byte="">(ref destination),
ref Unsafe.As<t, byte="">(ref source),
elementCount * (nuint)Unsafe.SizeOf<t>());
}
else
{
// Non-blittable memmove
BulkMoveWithWriteBarrier(
ref Unsafe.As<t, byte="">(ref destination),
ref Unsafe.As<t, byte="">(ref source),
elementCount * (nuint)Unsafe.SizeOf<t>());
}
}
以上性能測試使用的是 int 數組,剛好能進入 Memmove 的分支,而不是 BulkMoveWithWriteBarrier 這個分支。在里層的 Memmove 方法里面用到了很多黑科技,本文只是用來對比多個方法拷貝數組的性能,黑科技部分就需要大家自己去閱讀 dotnet 的源代碼啦
另外,如果需要做完全的數組的拷貝,數組里面存放的是值類型對象,如 int 類型,那么拷貝整個數組還有另一個可選項是通過 Clone
方法進行拷貝,代碼如下
public object CopyByClone()
{
var data = (int[]) TestData.Clone();
return data;
}
使用 Clone
的方法的行為是返回數組的淺表拷貝,也就是說數組里面的元素沒有做深拷貝,只是拷貝數組本身而已。對於值類型來說,就沒有啥問題了
稍微更改一下性能測試,更改的代碼如下
[MemoryDiagnoser]
public class Program
{
static void Main(string[] args)
{
BenchmarkRunner.Run<program>();
}
static Program()
{
TestData = new int[1000];
for (int i = 0; i < 1000; i++)
{
TestData[i] = i;
}
}
[Benchmark]
public object CopyByFor()
{
var rawPacketData = TestData;
var length = TestData.Length;
var data = new int[length];
for (int localIndex = 0, rawArrayIndex = 0; localIndex < data.Length; localIndex++, rawArrayIndex++)
{
data[localIndex] = rawPacketData[rawArrayIndex];
}
return data;
}
[Benchmark]
public object CopyByArray()
{
var length = TestData.Length;
var start = 0;
var rawPacketData = TestData;
var data = new int[length];
Array.Copy(rawPacketData,start,data,0, length);
return data;
}
[Benchmark]
public object CopyByClone()
{
var data = (int[]) TestData.Clone();
return data;
}
private static readonly int[] TestData;
}
通過下圖可以了解到采用 Clone 方法和采用 Array.Copy 方法的性能差不多,但 Clone 稍微快一點
以上是給 WPF 框架做性能優化時測試的,詳細請看
- Using
Array.Copy
to make array copy faster in StylusPointCollection by lindexi · Pull Request #5217 · dotnet/wpf - Using the
Clone
method to fast clone the array in StylusPoint by lindexi · Pull Request #5218 · dotnet/wpf
特別感謝ThomasGoulet73大佬教我使用 AsSpan 的方法拷貝數組