经验首页 前端设计 程序设计 Java相关 移动开发 数据库/运维 软件/图像 大数据/云计算 其他经验
当前位置:技术经验 » 程序设计 » C# » 查看文章
C#高性能数组拷贝实验
来源:cnblogs  作者:程序设计实验室  时间:2023/1/30 15:21:30  对本文有异议

前言

昨天 wc(Wyu_Cnk) 提了个问题

C# 里多维数组拷贝有没有什么比较优雅的写法?

这不是问对人了吗?正好我最近在搞图像处理,要和内存打交道,我一下就想到了在C#里面直接像C/C++一样做内存拷贝。

优雅?no,要的就是装逼,而且性能还要强??

概念

首先澄清一下

C# 里的多维数组 (Multi-dimensional Array) 是这样的

  1. byte[,] arr = new byte[10, 10];

下面这种写法是交错数组 (Jagged Array),就是数组里面套着数组

  1. byte[][] arr = new byte[10][];

具体区别请看文末的参考资料~

开始

接下来介绍几种拷贝数组的方法,然后再比较一下不同实现的性能

定义一下常量,SIZE 表示数组大小,COUNT 表示等会要做拷贝测试的循环次数

  1. const int COUNT = 32, SIZE = 32 << 20;

这里用了移位操作,32左移20位就是在32的二进制数后面补20个0,相当于 32*2^20,只是用来定义一个比较大的数,现在的电脑性能太强了,小一点的数组复制起来太快了,看不出区别。

接着定义几个数组,这里写了五组一维数组,每个不同的数组拷贝方法测试用不同的数组,这样可以避免CPU缓存。

  1. private static byte[]
  2. aSource = new byte[SIZE],
  3. aTarget = new byte[SIZE],
  4. bSource = new byte[SIZE],
  5. bTarget = new byte[SIZE],
  6. cSource = new byte[SIZE],
  7. cTarget = new byte[SIZE],
  8. dSource = new byte[SIZE],
  9. dTarget = new byte[SIZE],
  10. eSource = new byte[SIZE],
  11. eTarget = new byte[SIZE];

然后把这几个数组拷贝方法都测试一下

  • Clone方式: array.Clone()
  • Linq: array.Select(x=>x).ToArray()
  • Array.Copy()
  • Buffer.BlockCopy()
  • Buffer.MemoryCopy()

Clone 方式

在C#中,只要实现了 ICloneable 接口的对象,就有 Clone 方法

所以数组也可以通过这种方式来实现拷贝

很简单,直接 var newArray = (byte[])array.Clone() 就行了

代码如下

  1. static void TestArrayClone() {
  2. var sw = Stopwatch.StartNew();
  3. sw.Start();
  4. for (var i = 0; i < COUNT; i++) {
  5. dTarget = (byte[])dSource.Clone();
  6. }
  7. sw.Stop();
  8. Console.WriteLine("Array.Clone: {0:N0} ticks, {1} ms", sw.ElapsedTicks, sw.ElapsedMilliseconds);
  9. }

这里用了 Stopwatch 来记录执行时间,后面的其他拷贝方法里面也有,等会用这个计算出来的 ticks 和毫秒,可以比较不同实现的性能差距。

Linq方式

其实不用测试也知道这个方式是最慢的

就是一个个元素遍历,再重新构造个新的数组

代码如下

  1. eTarget = eSource.Select(x => x).ToArray();

Array.Copy()

使用静态方法 Array.Copy() 来实现数组复制

提示:性能是不错的,使用也方便

代码如下,只需要指定长度即可

  1. Array.Copy(cSource, cTarget, SIZE);

或者用另一个重载,可以分别指定两个数组的偏移值

  1. Array.Copy(cSource, 0, cTarget, 0, SIZE);

Buffer.BlockCopy()

Buffer 类是用来操作基本类型数组的

Manipulates arrays of primitive types.

代码如下

  1. Buffer.BlockCopy(bSource, 0, bTarget, 0, SIZE);

跟上面的 Array.Copy 第二个重载一样,需要分别指定两个数组的偏移值

Buffer.MemoryCopy()

这个是 unsafe 方法,需要用到指针 ?? 理论上是性能最好的

我最喜欢的就是这个方法(逼格高)

使用 unsafe 代码,请先在编译选项里面开启 allow unsafe code 选项。

这个 MemoryCopy 方法的函数签名是这样的

  1. static unsafe void MemoryCopy(void* source, void* destination, long destinationSizeInBytes, long sourceBytesToCopy)

前两个参数是指针类型,后俩个是长度,注意是bytes字节数,不是数组的元素个数

C#中的byte占8bit,刚好是一个byte,所以直接用元素个数就行,如果是其他类型的数组,得根据类型长度计算字节数,然后再传进去。

代码如下,在函数定义里面加上unsafe关键字以使用 fixed 块和指针

  1. static unsafe void TestBufferMemoryCopy() {
  2. var sw = Stopwatch.StartNew();
  3. fixed (byte* pSrc = fSource, pDest = fTarget) {
  4. for (int i = 0; i < COUNT; i++) {
  5. Buffer.MemoryCopy(pSrc, pDest, SIZE, SIZE);
  6. }
  7. }
  8. Console.WriteLine("Buffer.MemoryCopy (2d): {0:N0} ticks, {1} ms", sw.ElapsedTicks, sw.ElapsedMilliseconds);
  9. }

然后

我在搜索资料的过程中还发现了有人用了 Buffer.Memcpy 这个方法,但这个是 internal 方法,没有开放,得用黑科技去调用

我折腾了很久,终于搞出了调用非公开方法的代码

  1. unsafe delegate void Memcpy(byte* src, byte* dest, int len);
  2. internal class Program {
  3. private static Memcpy memcpy;
  4. static Program() {
  5. var methodInfo = typeof(Buffer).GetMethod(
  6. "Memcpy",
  7. BindingFlags.Static | BindingFlags.NonPublic,
  8. null,
  9. new Type[] { typeof(byte*), typeof(byte*), typeof(int) },
  10. null
  11. );
  12. if (methodInfo == null) {
  13. Console.WriteLine("init failed! method is not found.");
  14. return;
  15. }
  16. memcpy = (Memcpy)Delegate.CreateDelegate(typeof(Memcpy), methodInfo);
  17. }
  18. }

实际测试这个 MemcpyMemoryCopy 的性能是差不多的

看了一下.NetCore的源码

果然,这俩个的实现基本是一样的

  1. // Used by ilmarshalers.cpp
  2. internal static unsafe void Memcpy(byte* dest, byte* src, int len)
  3. {
  4. Debug.Assert(len >= 0, "Negative length in memcpy!");
  5. Memmove(ref *dest, ref *src, (nuint)(uint)len /* force zero-extension */);
  6. }

另一个

  1. public static unsafe void MemoryCopy(void* source, void* destination, long destinationSizeInBytes, long sourceBytesToCopy)
  2. {
  3. if (sourceBytesToCopy > destinationSizeInBytes) { ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.sourceBytesToCopy);
  4. }
  5. Memmove(ref *(byte*)destination, ref *(byte*)source, checked((nuint)sourceBytesToCopy));
  6. }

这俩最终都是调用的 Memmove 这个方法

区别就是这俩方法的参数不一样了。

benchmark

性能测试结果

  1. Array.Copy: 49,923,612 ticks, 49 ms
  2. Buffer.BlockCopy: 52,497,377 ticks, 52 ms
  3. Buffer.Memcpy: 49,067,555 ticks, 49 ms
  4. Buffer.MemoryCopy (2d): 48,982,014 ticks, 48 ms
  5. Array.Clone: 360,640,218 ticks, 360 ms
  6. Linq: 1,988,890,052 ticks, 1988 ms
  7. Array.Copy: 48,653,699 ticks, 48 ms
  8. Buffer.BlockCopy: 48,040,093 ticks, 48 ms
  9. Buffer.Memcpy: 47,818,057 ticks, 47 ms
  10. Buffer.MemoryCopy (2d): 49,084,413 ticks, 49 ms
  11. Array.Clone: 406,848,666 ticks, 406 ms
  12. Linq: 1,943,498,307 ticks, 1943 ms
  13. Array.Copy: 48,943,429 ticks, 48 ms
  14. Buffer.BlockCopy: 47,989,824 ticks, 47 ms
  15. Buffer.Memcpy: 48,053,817 ticks, 48 ms
  16. Buffer.MemoryCopy (2d): 49,065,368 ticks, 49 ms
  17. Array.Clone: 364,339,126 ticks, 364 ms
  18. Linq: 1,999,189,800 ticks, 1999 ms
  19. Array.Copy: 49,679,913 ticks, 49 ms
  20. Buffer.BlockCopy: 48,651,877 ticks, 48 ms
  21. Buffer.Memcpy: 48,262,443 ticks, 48 ms
  22. Buffer.MemoryCopy (2d): 49,683,361 ticks, 49 ms
  23. Array.Clone: 429,384,291 ticks, 429 ms
  24. Linq: 1,932,109,712 ticks, 1932 ms

该用哪个方法来拷贝数组,一目了然了吧~ ??

参考资料

原文链接:https://www.cnblogs.com/deali/p/17075093.html

 友情链接:直通硅谷  点职佳  北美留学生论坛

本站QQ群:前端 618073944 | Java 606181507 | Python 626812652 | C/C++ 612253063 | 微信 634508462 | 苹果 692586424 | C#/.net 182808419 | PHP 305140648 | 运维 608723728

W3xue 的所有内容仅供测试,对任何法律问题及风险不承担任何责任。通过使用本站内容随之而来的风险与本站无关。
关于我们  |  意见建议  |  捐助我们  |  报错有奖  |  广告合作、友情链接(目前9元/月)请联系QQ:27243702 沸活量
皖ICP备17017327号-2 皖公网安备34020702000426号