通过对C#文档中Array.Copy()和Buffer.BlockCopy()两个方法的整理,深刻理解其运行原理及实现方式。在理解了Array.Copy()之后,从而对List<T>的拷贝、插入、删除操作实现,有一个更深入的理解。
一、Array.Copy的详细文档
从给定的索引开始复制数组中的一系列元素,将它们粘贴到另一数组中(从给定的开始复制的索引开始)
语法
public static void Copy(
Array sourceArray,
long sourceIndex,
Array destinationArray,
long destinationIndex,
long length
)
参数
sourceArray
类型: System.Array
包含要复制的数据的数组。
sourceIndex
类型: System.Int64
一个64位整数,表示复制开始时sourceArray中的索引。
destinationArray
类型: System.Array
接收数据的数组。
destinationIndex
类型: System.Int64
一个64位整数,表示存储开始的destinationArray中的索引。
length
类型: System.Int64
一个表示要复制的元素数的64位整数。整数必须介于零和Int32之间。MaxValue,包括在内。
备注
该sourceArray和destinationArray参数必须具有相同的维数。另外,destinationArray必须已经被标注尺寸并且必须具有足够数量的从destinationIndex位置开始的元素以容纳复制的数据。
在多维数组之间进行复制时,数组的操作就像一个很长的一维数组,其中行(或列)在概念上是端对端放置的。例如,如果一个数组有三行(或列),每行有四个元素,从数组开头复制六个元素将复制第一行(或列)的所有四个元素和第二行的前两个元素(或列)。要从第三行(或列)的第二个元素开始复制,sourceIndex必须是第一行(或列)的上限加上第二行(或列)的长度加上两个。
如果sourceArray和destinationArray重叠,则此方法的操作就好像在覆盖destinationArray之前sourceArray的原始值已保存在临时位置中。
[C / C ++]
这个方法相当于标准的C / C ++函数memmove,而不是memcpy。
memmove与memcpy区别
memcpy()和memmove()都是C语言中的库函数,在头文件string.h中,作用是拷贝一定长度的内存的内容。原型如下:
void *memcpy(void *dst, const void *src, size_t count);
void *memmove(void *dst, const void *src, size_t count);
他们的作用是一样的,唯一的区别是,当内存发生局部重叠的时候,memmove保证拷贝的结果是正确的,memcpy不保证拷贝的结果的正确。
内存覆盖的情形有以下两种:
memcpy()和memmove()这两个函数的实现:
void* my_memcpy(void* dst, const void* src, size_t n)
{
Assert(dst != null && src != null);
char *tmp = (char*)dst;
char *s_src = (char*)src;
while(n--) {
*tmp++ = *s_src++;
}
return dst;
}
从实现中可以看出memcpy()是从内存左侧一个字节一个字节地将src中的内容拷贝到dst的内存中,这种实现方式导致了对于图中第二种内存重叠情形下,最后两个字节的拷贝值明显不是原先的值了,新的值是变成了src的最开始的2个字节了。
而memmove就是针对第二种内存覆盖情形,对memcpy进行了改进,改进代码如下:
void* my_memmove(void* dst, const void* src, size_t n)
{
Assert(dst != null && src != null);
char* s_dst;
char* s_src;
s_dst = (char*)dst;
s_src = (char*)src;
if(s_dst > s_src && (s_src + n > s_dst)) { // 第二种内存覆盖的情形。
s_dst = s_dst+n-1;
s_src = s_src+n-1;
while(n--) {
*s_dst-- = *s_src--;
}
}else {
while(n--) {
*s_dst++ = *s_src++;
}
}
return dst;
}
数组可以是引用类型数组或值类型数组,根据需要执行类型向下转换。
从引用类型数组复制到值类型数组时,每个元素都将被拆箱并复制。从值类型数组复制到引用类型数组时,每个元素都被装箱并复制。
从引用类型或值类型数组复制到Object数组时,会创建一个Object以保存每个值或引用,然后进行复制。从Object数组复制到引用类型或值类型数组是不可能进行赋值的,会抛出InvalidCastException。
如果sourceArray和destinationArray都是引用类型的数组,或者都是Object类型的数组,则会执行浅拷贝。Array的浅拷贝是一个新的Array,它包含对原始Array的相同元素的引用。元素本身或元素引用的任何内容都不会被复制。相比之下,Array的深层副本会复制由元素直接或间接引用的元素和所有内容。
例子
int count = 10000;
int[,] arr1 = new int[count, count];
for (int i = 0; i < count; i++)
{
for (int j = 0; j < count; j++)
{
arr1[i, j] = i;
}
}
int[,] arr2 = new int[count, count];
Array.Copy(arr1, 0, arr1, 1000, count);
二、Buffer.BlockCopy的详细文档
语法
将从指定偏移量开始的源数组中指定数量的字节复制到以特定偏移量开始的目标数组。
public static void BlockCopy(
Array src,
int srcOffset,
Array dst,
int dstOffset,
int count
)
参数
src
类型: System.Array
源缓冲区。
srcOffset
类型: System.Int32
基于零的字节偏移到src中。
dst
类型: System.Array
目标缓冲区。
dstOffset
类型: System.Int32
基于零的字节偏移到dst中。
count
类型: System.Int32
要复制的字节数。
备注
该方法将在src从srcOffset索引开始复制count字节,从dst的索引dstOffset开始复制到dst。srcOffset和dstOffset都是从零开始的; 也就是说,每个缓冲区中的第一个字节位于位置0,而不是位置1。
该 BlockCopy方法访问中的字节的src使用的在内存中的偏移,而不是程序结构,如索引或上部和下部数组边界参数阵列。例如,如果在应用程序的编程语言中声明一个Int32数组,其下限为-50,然后将该数组和偏移量5传递给BlockCopy方法,该方法将访问第一个数组元素是数组的第二个元素,它位于索引-49处。此外,首先访问数组元素-49索引下的那一个字节取决于执行应用程序的计算机字节序。
顾名思义,BlockCopy方法复制一个字节块作为一个整体,而不是一次复制一个字节。因此,如果当src和dst引用相同的数组时,并且srcOffset + count -1的范围与dstOffset + count -1 的范围重叠,则重叠字节的值在复制到目标之前不会被覆盖。
例子
在以下示例中,名为arr的数组中的字节0-16的值被复制到字节12-28。尽管范围重叠,但源字节的值已成功复制。
const int INT_SIZE = 4;
int[] arr = { 2, 4, 6, 8, 10, 12, 14, 16, 18, 20 };
Buffer.BlockCopy(arr, 0 * INT_SIZE, arr, 3 * INT_SIZE, 4 * INT_SIZE);
foreach (int value in arr)
Console.Write("{0} ", value);
// The example displays the following output:
// 2 4 6 2 4 6 8 16 18 20
三、测试
1.1 针对多维数组的性能测试
测试数据int[,] arr1 = new int[10000, 10000];,拷贝全部数据
| Array.Copy()执行结果: | Buffer.BlockCopy()执行结果: |
同一数组拷贝有重叠 | 1359136 | 1250880 |
同一数组拷贝不重叠 | 1420934 | 1306125 |
不同数组 | 1397174 | 1345293 |
并经过多次测试,Buffer.BlockCopy()执行效率优于Array.Copy()。
1.2 针对一维数组的性能测试
测试数据200MB 的int[],一次拷贝100MB
| Array.Copy()执行结果: | Buffer.BlockCopy()执行结果: |
同一数组拷贝有重叠 | 431120 | 426114 |
同一数组拷贝不重叠 | 421117 | 421088 |
不同数组 | 421083 | 421134 |
综上所述,Buffer.BlockCopy()的执行效率与Array.Copy()基本类似。
四、拨乱发正
上面的性能测试,Buffer.BlockCopy(arr, offSet, arr1, offSet1, count)中的offSet , offSet1和count都是字节在内存中偏移量,offSet , offSet1不是数组索引,count也不是数组的数据长度。
在刚开始的多维数组和一维数组的拷贝测试中,直接把数组的数据长度count传入当做参数,Buffer.BlockCopy()执行效率比Array.Copy()好很多,尤其在一维数组的拷贝测试中,大约有4倍的优势。其实,这样的测试,Buffer.BlockCopy()方法并没有把数据全部拷贝完成,只拷贝了1/4,所以才产生了如此大的差距。
传入Buffer.BlockCopy()的count应该数组的数据长度乘以4,因为int占4个字节,count代表的是字节在内存中偏移量。在传入正确的count后,一维数组拷贝测试,两者的性能基本没有差异;多维数组的拷贝测试,还是有一定的差距,Buffer.BlockCopy()执行效率优于Array.Copy()。
按说Buffer.BlockCopy()执行效率优于Array.Copy(),为什么还要存在Array.Copy()接口?
因为Buffer.BlockCopy()访问数组元素索引下的字节取决于执行应用程序的计算机字节序。
计算机硬件有两种储存数据的方式:大端字节序(big endian)和小端字节序(little endian)。
比如十六进制数0x0102,十六进制每个数字占4bit,四个数字16bit,也就是2byte,2个字节存储。0x0102高位字节是0x01,低位字节是0x02。
l 大端字节序:高位字节在前,低位字节在后,这是人类读写数值的方法。
内存地址 | 0x100 | 0x101 |
大端字节序 | 01 | 02 |
l 小端字节序:低位字节在前,高位字节在后。
内存地址 | 0x100 | 0x101 |
小端字节序 | 02 | 01 |
因此,在大端字节序和小端字节序不同的计算机设备上,Buffer.BlockCopy()拷贝的数据是不同的,在大端字节序设备上,拷贝的数据是0x0102;在小端字节序设备上,拷贝的数据是0x0201,所以Array.Copy()接口存在的意义就在这里了。
请看下面的例子,当只拷贝一个字节的长度,复制给arr1数组时,因为Windows是基于小端字节序的,所以只拷贝了0x02到了arr[0]的第一个偏移位置。
下面的四个格子,代表int的四个字节
arr[0] | |||
02 | 01 | 0 | 0 |
Arr1[0] | |||
0 | 02 | 0 | 0 |
arr1[0]的值就是0x0200,转化为十进制就是512
static void Main(string[] args)
{
int[] arr = new int[10];
int[] _arr1 = new int[10];
arr[0] = 0x0102;//十进制 258
//最后一个count是字节在内存中的偏移量
Buffer.BlockCopy(arr, 0, _arr1, 1, 1);
Console.WriteLine(arr[0]);//十进制 258
Console.WriteLine(_arr1[0]);//十进制 512
}
因此,如果错误的传入数组的数据长度,会导致数据并没有拷贝完整。