各种数组元素复制方式的性能比较
2009-12-02 10:27 Jeffrey Zhao 阅读(9365) 评论(64) 编辑 收藏 举报原本这只是“字符串”话题的一个分支,不过后来我发现这个问题单独来看也有一定参考价值,也有一些问题值得讨论的地方,其中也有一些问题希望得到高手指点,最终打算把这个话题独立处理。话不多说,现在就来看看。
这个话题是“复制数组元素”,它最简单的情况也就是把源数组的所有元素,一一复制到等长目标数组中去。在.NET中,我们可以使用多种方法来实现这个效果:
- 使用for语句一个个复制元素。
- 使用数组的CopyTo方法复制元素。
- 使用Array.Copy静态复制元素。
- 使用Buffer.BlockCopy复制元素。
于是使用如下代码进行实验:
int length = 1000 * 1000; var source = Enumerable.Repeat(100, length).ToArray(); var dest = new int[length]; int iteration = 1000; CodeTimer.Initialize(); CodeTimer.Time("One by one", iteration, () => { for (int i = 0; i < length; i++) dest[i] = source[i]; }); CodeTimer.Time("Int32[].CopyTo", iteration, () => { source.CopyTo(dest, 0); }); CodeTimer.Time("Array.Copy", iteration, () => { Array.Copy(source, dest, length); }); CodeTimer.Time("Buffer.BlockCopy", iteration, () => { Buffer.BlockCopy(source, 0, dest, 0, length * 4); });
运行结果如下(GC都是0,就省略了):
One by one Time Elapsed: 3,153ms CPU Cycles: 7,570,978,116 Int32[].CopyTo Time Elapsed: 2,933ms CPU Cycles: 7,056,050,496 Array.Copy Time Elapsed: 3,018ms CPU Cycles: 7,244,562,468 Buffer.BlockCopy Time Elapsed: 2,925ms CPU Cycles: 7,014,908,760
我们发现,每种方法的性能其实都差不多,多次运行后发现,除了第一种方法(一一复制)的性能每次都会略差一些外,其他三种方法基本总是不相上下,时有胜负(可能Array.Copy和Buffer.BlockCopy胜利次数略多一些)。这和我起初的想象又有不同。我一开始认为后三种方法应该有较大领先才对,而Buffer.BlockCopy应该性能最好。因为在进行元素一一复制的时候,需要进行大量的下标计算,而每次读取和写入的时候也会为了避免GC造成对象地址变动而进行一些“固定”操作。而Array.Copy和Buffer.BlockCopy两个静态方法都是外部调用,CLR应该会为它们进行性能优化才对。
不过猜测总是敌不过测试数据——有机会还是看看汇编吧,用.NET Reflector看不出个所以然来。
那么,我们在实际使用过程中,应该使用哪种方式呢?
一一复制的方式最为灵活,您可以正着来,反着来,跳着来,爱咋来咋来;而CopyTo的灵活性最差,因为它只能复制数组的全部元素(事实上,CopyTo方法是ICollection接口的一部分);Array.Copy静态方法的作用是把原数组中连续的一段,复制到目标数组中去,如果您不需要蹦蹦跳跳地前进,用Array.Copy是比较理想辅助函数。而最后一个Buffer.BlockCopy,它其实并不是在复制“元素”,它复制的是“字节”——因此,在上面的代码中,BlockCopy的最后一个参数为length * 4,这是因为一个32位整数占4个字节,其实我要复制的并非是length个元素,而是要复制length * 4个字节。
如下面的代码则是将一个长为100的int数组,复制到了长为100的long数组中去。此时,目标数组的每个元素为100L:
int length = 100; var source = Enumerable.Repeat(10, length).ToArray(); var destLong = new long[length]; Array.Copy(source, destLong, length);
而下面的代码,是把长度100的int数组的所有字节,复制到长为50的long数组里去。由于是字节级别的填充,因此目标数组的每个元素是42949672970L:
var destLong = new long[length / 2]; Buffer.BlockCopy(source, 0, destLong, 0, length * 4);
那么将int(32位整数)数组复制到byte(8位)或short(16位)上会是什么结果呢?会不会受到字节序的影响,而造成不同平台上的结果差异呢?我猜,应该不会吧,CLR在平台差异性问题上解决的其实不错。那如果没有区别的话,CLR又是怎么处理这个情况的呢?我不清楚,似乎也没有什么条件来实验,还是请高手来指点吧。
那么,下面的代码又会是什么情况呢?objest数组的每个元素都指到哪儿去了呢?
var objs = new object[length]; Buffer.BlockCopy(source, 0, objs, 0, length * 4);
结果是“抛出异常”。因为Buffer.BlockCopy只能复制基础类型(primitive)的数组,不能复制引用,也不能复制各种复杂的struct类型。因此,其实Buffer.BlockCopy理论上不能算是一种“数组元素复制方式”。
补充测试(引用数组)
经RFX大牛提示,在引用复制时会为了避免GC造成影响而使用Write Barrier,因此使用引用类型进行测试会有明显的效果。于是我们来补充一个测试:
int length = 1000 * 1000; var source = Enumerable.Repeat(new object(), length).ToArray(); var dest = new object[length]; int iteration = 1000; CodeTimer.Initialize(); CodeTimer.Time("One by one", iteration, () => { for (int i = length - 1; i >= 0; i--) dest[i] = source[i]; }); CodeTimer.Time("Int32[].CopyTo", iteration, () => { source.CopyTo(dest, 0); }); CodeTimer.Time("Array.Copy", iteration, () => { Array.Copy(source, dest, length); });
结果如下:
One by one Time Elapsed: 7,298ms CPU Cycles: 17,523,631,872 Int32[].CopyTo Time Elapsed: 4,117ms CPU Cycles: 9,823,501,236 Array.Copy Time Elapsed: 4,225ms CPU Cycles: 10,119,239,496
果然,我又弱了……