前言

昨天 wc(Wyu_Cnk) 提了个问题

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

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

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

概念

首先澄清一下

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

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

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

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

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

开始

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

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

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

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

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

private static byte[]
aSource = new byte[SIZE],
aTarget = new byte[SIZE],
bSource = new byte[SIZE],
bTarget = new byte[SIZE],
cSource = new byte[SIZE],
cTarget = new byte[SIZE],
dSource = new byte[SIZE],
dTarget = new byte[SIZE],
eSource = new byte[SIZE],
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() 就行了

代码如下

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

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

Linq方式

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

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

代码如下

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

Array.Copy()

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

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

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

Array.Copy(cSource, cTarget, SIZE);

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

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

Buffer.BlockCopy()

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

Manipulates arrays of primitive types.

代码如下

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

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

Buffer.MemoryCopy()

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

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

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

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

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

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

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

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

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

然后

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

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

unsafe delegate void Memcpy(byte* src, byte* dest, int len);

internal class Program {
private static Memcpy memcpy;
static Program() {
var methodInfo = typeof(Buffer).GetMethod(
"Memcpy",
BindingFlags.Static | BindingFlags.NonPublic,
null,
new Type[] { typeof(byte*), typeof(byte*), typeof(int) },
null
);
if (methodInfo == null) {
Console.WriteLine("init failed! method is not found.");
return;
} memcpy = (Memcpy)Delegate.CreateDelegate(typeof(Memcpy), methodInfo);
}
}

实际测试这个 Memcpy 和 MemoryCopy 的性能是差不多的

看了一下.NetCore的源码

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

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

另一个

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

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

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

benchmark

性能测试结果

Array.Copy: 49,923,612 ticks, 49 ms
Buffer.BlockCopy: 52,497,377 ticks, 52 ms
Buffer.Memcpy: 49,067,555 ticks, 49 ms
Buffer.MemoryCopy (2d): 48,982,014 ticks, 48 ms
Array.Clone: 360,640,218 ticks, 360 ms
Linq: 1,988,890,052 ticks, 1988 ms Array.Copy: 48,653,699 ticks, 48 ms
Buffer.BlockCopy: 48,040,093 ticks, 48 ms
Buffer.Memcpy: 47,818,057 ticks, 47 ms
Buffer.MemoryCopy (2d): 49,084,413 ticks, 49 ms
Array.Clone: 406,848,666 ticks, 406 ms
Linq: 1,943,498,307 ticks, 1943 ms Array.Copy: 48,943,429 ticks, 48 ms
Buffer.BlockCopy: 47,989,824 ticks, 47 ms
Buffer.Memcpy: 48,053,817 ticks, 48 ms
Buffer.MemoryCopy (2d): 49,065,368 ticks, 49 ms
Array.Clone: 364,339,126 ticks, 364 ms
Linq: 1,999,189,800 ticks, 1999 ms Array.Copy: 49,679,913 ticks, 49 ms
Buffer.BlockCopy: 48,651,877 ticks, 48 ms
Buffer.Memcpy: 48,262,443 ticks, 48 ms
Buffer.MemoryCopy (2d): 49,683,361 ticks, 49 ms
Array.Clone: 429,384,291 ticks, 429 ms
Linq: 1,932,109,712 ticks, 1932 ms

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

参考资料

C#高性能数组拷贝实验的更多相关文章

  1. java杂记——数组拷贝

    这里介绍两种java提供的数组拷贝方法: (1)Arrays提供的copyOf(T src, T desLength)和copyOfRange(T src, int from, int to) (2) ...

  2. Java基础知识强化85:System类之arraycopy()方法(数组拷贝)

    1. arraycopy方法(数组拷贝) public static void arraycopy(object src,int srcPos,Object dest,int destPos, int ...

  3. JavaScript 高性能数组去重

    中午和同事吃饭,席间讨论到数组去重这一问题 我立刻就分享了我常用的一个去重方法,随即被老大指出这个方法效率不高 回家后我自己测试了一下,发现那个方法确实很慢 于是就有了这一次的高性能数组去重研究 一. ...

  4. C#编程(七十六)----------使用指针实现基于栈的高性能数组

    使用指针实现基于栈的高性能数组 以一个案例为主来分析实现方法: using System; using System.Collections.Generic; using System.Linq; u ...

  5. Java 数组拷贝方法 System.arraycopy

    System类提供的数组拷贝方法: public static native void arraycopy(Object src, int srcPos, Object dest, int destP ...

  6. Java数组拷贝的五种方法

    在Java中有多种方法可以拷贝一个数组,到另外一个数组. 1.循环拷贝 在循环拷贝方法中,只需要利用i,移动指针即可复制所有数组到arrayB中. for(int i=0;i<arrayA.le ...

  7. js 数组拷贝与深拷贝

    1.对于普通数组(数组元素为数字或者字符串) var _testCopy = [1,2,3].concat();//拷贝数组(浅拷贝) 2.对于对象数组 (深拷贝) //形如var _objArr=[ ...

  8. String 字符串的追加,数组拷贝

    package chengbaoDemo; import java.util.Arrays; /** *需求:数组的扩容以及数据的拷贝 *分析:因为String的实质是以字符数组存储的,所以字符串的追 ...

  9. javascript : 复杂数据结构拷贝实验

    数组深拷贝看起来很简单. array.concat()就行了. 但是,如果数组里有对象呢? 实际上,你以为你拷贝了对象,但实际上你只拷贝了对象的引用(指针)! 我们可以做个试验. // test le ...

  10. 数组拷贝 copyOf()

    Arrarys类的copyof方法与copyOfRange方法可以实现对数组的复制,前者是复制数组到指定的长度,后者将指定的长度复制到一个新数组中. 1.copyOf()方法 该方法提供了很多种重载形 ...

随机推荐

  1. Workflow,要不要了解一下

    摘要:Workflow本质是开发者基于实际业务场景开发用于部署模型或应用的流水线工具. Workflow(也称工作流,下文中均可使用工作流进行描述)本质是开发者基于实际业务场景开发用于部署模型或应用的 ...

  2. linux重置密码

    方法一: 进入grub菜单界面 按e键 在linux开头的行按ctrl+e 或者end跳到行尾,输入rd.break 按ctrl+x mount -o remount,rw /sysroot chro ...

  3. CSS 动画一站式指南

    CSS 动画一站式指南 目录 CSS 动画一站式指南 1. CSS 动画 1.1 变换 1.1.1 变换属性介绍 1.1.2 变换动画实践 1.2 过渡 1.2.1 过渡属性介绍 1.2.2 过渡动画 ...

  4. Redis 先操作数据库和先删除缓存, 一致性分析

    初始状态: 数据库和缓存中v=10 第一种,先删除缓存在操作数据库: 线程1准备更新数据库的值v=20,先删除缓存, 此时线程2进来, 缓存未命中,查询数据库v=10, 写入缓存v=10, cpu回到 ...

  5. PYQT5 学习

    zetcode教程 汉化版: https://maicss.gitbook.io/pyqt5-chinese-tutoral/ 官方网站: https://www.riverbankcomputing ...

  6. MyBatis&Spring Framrwork

    1. MyBatis 1.1 概述 MyBatis是一款优秀的持久层框架,用于简化JDBC开发 MyBatis本是Apache的一个开源项目iBatis,2010年这个项目迁移到了google cod ...

  7. 2022-11-01 Acwing每日一题

    第k个数 给定一个长度为 n 的整数数列,以及一个整数 k,请用快速选择算法求出数列从小到大排序后的第 k 个数. 输入格式 第一行包含两个整数 n 和 k. 第二行包含 n 个整数(所有整数均在 1 ...

  8. srcddd

    目录 application assembler UserReq.go UserRsp.go dto MessageResult.go UserDTO.go services UserService. ...

  9. Conda 环境移植 (两种方式)

    ------------------------方法一------------------------ 优点: 在原机器上需要进行的操作较少,且除了conda不需要其余的库来支撑:需要传输的文件小,操 ...

  10. Kubernetes_Deployment全解析(无状态的Pod)

    前言 一.创建Deployment 1.1 创建Deployment apiVersion: apps/v1 kind: Deployment metadata: name: nginx-deploy ...