代码改变世界

各种数组元素复制方式的性能比较

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

果然,我又弱了……