首页 文章详情

.NET 6 数组拷贝性能对比

DotNet NB | 268 2021-09-28 08:15 0 0 0
UniSMS (合一短信)

本文来对比多个不同的方法进行数组拷贝,和测试其性能

测试性能必须采用基准(标准)性能测试方法,否则测试结果不可信。在 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 的方法拷贝数组


推荐阅读:
Kubernetes全栈架构师(Kubeadm高可用安装k8s集群)--学习笔记
.NET 云原生架构师训练营(模块一 架构师与云原生)--学习笔记
.NET Core开发实战(第1课:课程介绍)--学习笔记

点击下方卡片关注DotNet NB

一起交流学习

▲ 点击上方卡片关注DotNet NB,一起交流学习

请在公众号后台


 回复 【路线图】获取.NET 2021开发者路线图
 回复 【原创内容】获取公众号原创内容
 回复 【峰会视频】获取.NET Conf开发者大会视频
 回复 【个人简介】获取作者个人简介
 回复 【年终总结】获取作者年终总结
 回复 加群加入DotNet NB 交流学习群

长按识别下方二维码,或点击阅读原文。和我一起,交流学习,分享心得。


good-icon 0
favorite-icon 0
收藏
回复数量: 0
    暂无评论~~
    Ctrl+Enter