Hello World
Spiga

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

2009-12-02 10:27 by 老赵, 7197 visits

原本这只是“字符串”话题的一个分支,不过后来我发现这个问题单独来看也有一定参考价值,也有一些问题值得讨论的地方,其中也有一些问题希望得到高手指点,最终打算把这个话题独立处理。话不多说,现在就来看看。

这个话题是“复制数组元素”,它最简单的情况也就是把源数组的所有元素,一一复制到等长目标数组中去。在.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

果然,我又弱了……

Creative Commons License

本文基于署名 2.5 中国大陆许可协议发布,欢迎转载,演绎或用于商业目的,但是必须保留本文的署名赵劼(包含链接),具体操作方式可参考此处。如您有任何疑问或者授权方面的协商,请给我留言

Add your comment

66 条回复

  1. 幸运草
    *.*.*.*
    链接

    幸运草 2009-12-02 10:29:00

    老赵给你发了两条短信息,你看一下,博客园的短信息

  2. 四有青年
    *.*.*.*
    链接

    四有青年 2009-12-02 10:59:00

    楼主的钻研精神值得学习啊

  3. 第一控制.NET
    *.*.*.*
    链接

    第一控制.NET 2009-12-02 11:07:00

    老赵迷上性能比较了。。。

  4. 老赵
    admin
    链接

    老赵 2009-12-02 11:11:00

    @第一控制.NET
    都是我想问题时涉及到的,不是故意去比较的。
    不过我这边如果可以收集常见各种做法性能,以后别人一捜就来我这儿,多好。

  5. 装配脑袋
    *.*.*.*
    链接

    装配脑袋 2009-12-02 11:14:00

    这次一定要抢到前排……

  6. 老赵
    admin
    链接

    老赵 2009-12-02 11:16:00

    @装配脑袋
    脑袋,“将int(32位整数)数组复制到byte(8位)或short(16位)上会是什么结果呢?会不会受到字节序的影响,而造成不同平台上的结果差异呢?”

  7. RednaxelaFX
    *.*.*.*
    链接

    RednaxelaFX 2009-12-02 11:22:00

    老赵能否在原帖中补充一下你比较的各种方法复制引用类型的数组时的性能比较?或许会有趣哦(因为会受write barrier的影响……)

  8. RednaxelaFX
    *.*.*.*
    链接

    RednaxelaFX 2009-12-02 11:26:00

    Jeffrey Zhao:
    将int(32位整数)数组复制到byte(8位)或short(16位)上会是什么结果呢?会不会受到字节序的影响,而造成不同平台上的结果差异呢?


    这句话读起来很怪,老赵能否重新组织一下语言?是说把int[]的内容复制到byte[]或short[]中么?你是希望怎样复制,对应元素truncate掉,还是例如说把int[n]复制到short[n*2]里?

  9. 老赵
    admin
    链接

    老赵 2009-12-02 11:28:00

    @RednaxelaFX
    啊,我的意思是用Buffer.BlockCopy作字节的复制。例如把int[100]复制为short[200]

  10. 老赵
    admin
    链接

    老赵 2009-12-02 11:31:00

    @RednaxelaFX
    我试试看,话说我就以为有barrier,所以一个一个元素复制会慢很多。

  11. 装配脑袋
    *.*.*.*
    链接

    装配脑袋 2009-12-02 11:36:00

    引用的复制不是原子操作,所以才需要障栅的吧?(我的意思是非引用不需要)

  12. 老赵
    admin
    链接

    老赵 2009-12-02 11:41:00

    @装配脑袋
    嗯,这可以理解,但是……非引用的复制为啥就是原子操作了呢?

  13. 装配脑袋
    *.*.*.*
    链接

    装配脑袋 2009-12-02 11:44:00

    哦。。我用的术语可能有问题……
    弱弱地飘过……

  14. Gnie
    *.*.*.*
    链接

    Gnie 2009-12-02 11:54:00

    老赵都开始研究这个啦~

  15. Ivony...
    *.*.*.*
    链接

    Ivony... 2009-12-02 11:56:00

    write barrier不能拆开来解释。。。。。所以你们讨论的不是一个问题。。。

  16. winter-cn
    *.*.*.*
    链接

    winter-cn 2009-12-02 12:51:00

    老赵最近开始研究基础效率问题liao~

  17. 老赵
    admin
    链接

    老赵 2009-12-02 12:52:00

    @winter-cn
    其实就是因为那个URL拼接的效率问题……

  18. RednaxelaFX
    *.*.*.*
    链接

    RednaxelaFX 2009-12-02 13:19:00

    @Jeffrey Zhao
    CLR里的write barrier有一个重要用途是为了支撑分代式GC(咦……我貌似在老赵以前什么帖里也说过?)

    假设我有一个object[] arr,目前arr已经活了很久,跑到Gen2了。此时来个arr[2] = new object(),刚new出来的对象肯定在Gen0里,于是就出现了从年老代指向年轻代的引用。如果只把静态变量、栈上变量和寄存器中的变量作为GC的根,那在做Gen0收集的时候就会漏掉这种从年老代指向年轻代的引用,就有问题了。

    如果是int[]就没这问题,因为它里面不包含指针。

    CLR记录这种从年老到年轻代的引用是通过card table来完成的。card table可以看成是一个大的bit数组,里面每个bit代表一块128字节的空间。给成员变量或数组元素赋值时,如果成员变量的类型或者数组元素的类型是引用类型(包装了的值类型也算引用类型),就会经过write barrier,把目标所在的对象对应到card table里的那个bit置位。GC的时候card table记录了有跨代指针的地方就会被作为GC根来扫描。

    for (int i = 0; i < arr1.Length; i++) {
      arr1[i] = arr2[i];
    }

    假设arr1、arr2都是object[]。老赵可以用!u命令看看这段代码生成出来的样子。我工作机上没VS也没WinDbg现在贴不了代码来演示。印象中这种代码应该是每轮循环都要经过一次write barrier。

    Array.Copy()的话是在数组元素整体复制完了之后再检查该数组里是否有指针,有的话整体在card table里置位,只经过一次类似write barrier的地方去设置card table。

  19. 老赵
    admin
    链接

    老赵 2009-12-02 13:26:00

    @RednaxelaFX
    对了,其实int[]的话,其实获取一个元素会根据偏移量计算地址,再取值是吧,那么如果在计算好地址后发生GC,那么不就……CLR应该会在计算前作些什么事情吧。

  20. RednaxelaFX
    *.*.*.*
    链接

    RednaxelaFX 2009-12-02 13:32:00

    @Jeffrey Zhao
    GC不会在任意点发生的……只会在safe point发生。放心好了。

    Jeffrey Zhao:
    @RednaxelaFX
    对了,其实int[]的话,其实获取一个元素会根据偏移量计算地址,再取值是吧,那么如果在计算好地址后发生GC,那么不就……CLR应该会在计算前作些什么事情吧。


    话说无论什么是int[]还是object[]还是啥别的数组,访问的逻辑都是一样计算偏移量再取值。只是赋值的时候对引用类型的元素赋值后要经过write barrier而已。

  21. Ivony...
    *.*.*.*
    链接

    Ivony... 2009-12-02 13:38:00

    @RednaxelaFX


    其实我所迷惑的是,如果用for的话,不是每次都要检查边界么?但从测试中看不到这个性能的损失。。。。。

  22. RednaxelaFX
    *.*.*.*
    链接

    RednaxelaFX 2009-12-02 13:42:00

    CLR的JIT在看到for (int i = 0; i < array.Length; i++) ...的时候会把边界检查外提的。但如果写成:

    int len = array.Length;
    for (int i = 0; i < len; i++) {
      // ...
    }

    你再看看…… =_=||||

    现在的CLR的JIT还不够强悍啊

  23. Ivony...
    *.*.*.*
    链接

    Ivony... 2009-12-02 13:44:00

    RednaxelaFX:
    CLR的JIT在看到for (int i = 0; i < array.Length; i++) ...的时候会把边界检查外提的。但如果写成:

    int len = array.Length;
    for (int i = 0; i < len; i++) {
      // ...
    }

    你再看看…… =_=||||

    现在的CLR的JIT还不够强悍啊




    强大的JIT,膜拜之。。。。。

  24. Duron800[未注册用户]
    *.*.*.*
    链接

    Duron800[未注册用户] 2009-12-02 13:45:00

    Ivony...:
    @RednaxelaFX


    其实我所迷惑的是,如果用for的话,不是每次都要检查边界么?但从测试中看不到这个性能的损失。。。。。


    我猜是像CLR VIA C#里说的被编译器给优化了吧。只检查一次。

  25. 老赵
    admin
    链接

    老赵 2009-12-02 13:46:00

    @RednaxelaFX
    哦哈哈

  26. 装配脑袋
    *.*.*.*
    链接

    装配脑袋 2009-12-02 13:55:00

    我只知道你们说的write barrier应该存在,因为我知道GC大概是怎么工作的,这只是一个技术直觉而已。但是write barrier到底是什么以及具体怎么工作我就不知道了,没有研究过其细节。

    我现在正在和GourpMemoryBarrierWithGroupSync之类的做斗争,呵呵。。

  27. 徐少侠
    *.*.*.*
    链接

    徐少侠 2009-12-02 13:55:00

    我也迷上了
    正打算搞一个通用性能测试平台
    谁叫先有的性能测试软件都那么难找,还收费
    主要搞本地dll性能测试

  28. 装配脑袋
    *.*.*.*
    链接

    装配脑袋 2009-12-02 14:02:00

    我倒是挺担心CLR组的那帮人,怎么处理日新月异的处理器技术。例如现在内存访问对多个核心来说都不均匀了。。

    看来编译器工作者充分就业原理总还是能够发挥作用,嘿嘿。

  29. RednaxelaFX
    *.*.*.*
    链接

    RednaxelaFX 2009-12-02 14:42:00

    好吧……那我写写。
    给元素类型为引用类型的数组的元素赋值时,并不是一句mov就完事了,而是调用了一个辅助函数,CORINFO_HELP_ARRADDR_ST(array, index, value),也就是array address store。这个函数的逻辑是:(伪代码)

    if (array == null) {
      // ... 抛NullReferenceException
    }
    
    if (index >= array.Length) { // 注意:这个实际是无符号比较,所以index小于0的情况也考虑了
      // ... 抛IndexOutOfRangeException
    }
    
    if (value != null) {
      // ... 检查赋值兼容性,不兼容时抛ArrayTypeMismatchException
      array[index] = value;
    
      /////// write barrier ///////
      if (value在年轻代) {
        // ... 如果array[index]所在位置在card table对应的里的bit为0则置位
      }
      /////////////////////////////
    } else {
      array[index] = value;
    }

    于是……就是这样的嗯。每当在C#里对元素为引用类型的array写array[index] = value时都会经过那个CORINFO_HELP_ARRADDR_ST。而Array.Copy()则是类似memcpy()那样一大块一大块的直接复制,最后再看要不要设card table,要的话整体设一次。

  30. 老赵
    admin
    链接

    老赵 2009-12-02 15:12:00

    话说我们来做个游戏吧,轮流给RFX出题,谁的问题RFX先答不出谁就赢,好不好?

  31. 装配脑袋
    *.*.*.*
    链接

    装配脑袋 2009-12-02 15:17:00

    我偷偷地去看看真正的内部实现。。

  32. RednaxelaFX
    *.*.*.*
    链接

    RednaxelaFX 2009-12-02 15:19:00

    装配脑袋:我偷偷地去看看真正的内部实现。。


    write barrier的核心部分是动态生成的,把边界条件写死在生成出来的代码里了。在每次GC改变分代边界时write barrier会重新生成。这个实现从头到尾是怎么来的有点不太好跟……

  33. 老赵
    admin
    链接

    老赵 2009-12-02 15:23:00

    @装配脑袋
    阿哦,脑袋看得到CLR源代码?

  34. RednaxelaFX
    *.*.*.*
    链接

    RednaxelaFX 2009-12-02 15:30:00

    Jeffrey Zhao:
    @装配脑袋
    阿哦,脑袋看得到CLR源代码?


    (FX两眼放光)

  35. Ivony...
    *.*.*.*
    链接

    Ivony... 2009-12-02 15:54:00

    一个URL引起的血案。。。。。

  36. Ivony...
    *.*.*.*
    链接

    Ivony... 2009-12-02 15:56:00

    RednaxelaFX:
    好吧……那我写写。
    给元素类型为引用类型的数组的元素赋值时,并不是一句mov就完事了,而是调用了一个辅助函数,CORINFO_HELP_ARRADDR_ST(array, index, value),也就是array address store。这个函数的逻辑是:(伪代码)
    [code=c]if (array == null) {
    // ... 抛NullReferenceException
    }

    if (index >= array.Length) { // 注意:这个实际是无符号比较,所以index小于0的情况也考虑了
    // ... 抛IndexOutOf...



    这么说来,由于数组的协变存在,引用类型的数组Array.Copy还不能是直接内存拷贝?因为要检查赋值兼容性?

  37. 老赵
    admin
    链接

    老赵 2009-12-02 16:23:00

    Ivony...:一个URL引起的血案。。。。。


    差不多优化完了,根据Route生成URL(就是第3阶段)的耗时大约变成了原来的1/5到1/4……

  38. 徐少侠
    *.*.*.*
    链接

    徐少侠 2009-12-02 16:57:00

    程序大家都能写出来
    后面就是性能优化了

    我觉得应该能总结出点结论,以后各个方面在编码的时候就应该尽量在编码效率和执行效率上合理平衡
    别经常用低效率的代码

  39. 徐少侠
    *.*.*.*
    链接

    徐少侠 2009-12-02 16:57:00

    所以追求效率细节是很有必要的

  40. RednaxelaFX
    *.*.*.*
    链接

    RednaxelaFX 2009-12-02 17:01:00

    Ivony...:
    这么说来,由于数组的协变存在,引用类型的数组Array.Copy还不能是直接内存拷贝?因为要检查赋值兼容性?


    Array.Copy()里会对数组的MethodTable做检查,当目标数组的元素类型是源数组的元素类型的子类型,就会进入单个单个引用复制的模式,每复制一个都检查一次看看能否正确cast。

  41. RednaxelaFX
    *.*.*.*
    链接

    RednaxelaFX 2009-12-02 17:02:00

    Jeffrey Zhao:

    Ivony...:一个URL引起的血案。。。。。


    差不多优化完了,根据Route生成URL(就是第3阶段)的速度大约变成了原来的1/5到1/4……


    速度降低了那么多么orz...根据Route生成URL果然太慢?

  42. 老赵
    admin
    链接

    老赵 2009-12-02 17:03:00

    @徐少侠
    总归有个度的,在平时积累一些性能上的经验,然后为了可读性,可维护性而开发。
    不必为了优化而优化,但也不要放过可维护性没有影响但是性能高的做法,比如Array.Copy……

    真说到优化,其实也就是性能瓶颈处值得优化,但是在开发过程中,大部分地方不是性能瓶颈。
    估计ASP.NET Routing在写的时候也没想到会对GetVirtualPath方法作密集调用,呵呵。

  43. 老赵
    admin
    链接

    老赵 2009-12-02 17:04:00

    @RednaxelaFX
    说错了说错了,是用我自己的实现,耗时是ASP.NET Routing自带的1/5……
    性能提高是蛮明显的,当然肯定还是比不过直接拼接字符串,呵呵。

  44. 老赵
    admin
    链接

    老赵 2009-12-02 17:18:00

    @RednaxelaFX
    不过其实发现逻辑还有错误,但性能总归是提高的吧。
    现在看起来,某些情况下可以提升4/5性能,某些情况下可以提高2/3……
    不过这代码真tmd晦涩啊……

  45. Ivony...
    *.*.*.*
    链接

    Ivony... 2009-12-02 18:06:00

    Jeffrey Zhao:
    @RednaxelaFX
    不过其实发现逻辑还有错误,但性能总归是提高的吧。
    现在看起来,某些情况下可以提升4/5性能,某些情况下可以提高2/3……
    不过这代码真tmd晦涩啊……




    贴出来看看哈,看看有啥可改进的。。。。

  46. 老赵
    admin
    链接

    老赵 2009-12-02 18:08:00

    @Ivony...
    这个需求的确很变态,就是有的default,有的么有,有的又可以省略,有的又要拼成QueryString……总之很令人发指。
    我也不知道完整需求如何,我只是写了一些测试,常用的情况下是正常的。
    我也不知道我这个到底还能不能改进,代码贴出来会很可怕……

  47. 装配脑袋
    *.*.*.*
    链接

    装配脑袋 2009-12-02 18:10:00

    想起当年和Ivony在CSDN上对一个小小的问题拼命优化,就为了那几毫秒的提升……

  48. 老赵
    admin
    链接

    老赵 2009-12-02 18:12:00

    @装配脑袋
    这……如何确定这不是误差呢……

  49. 老赵
    admin
    链接

    老赵 2009-12-02 19:20:00

    纯比拼接,时间节省70-80%。
    进行实际测试,实际节省50%-60%(因为还有些其它方面的消耗是节省不掉的)。
    因此,目前Fluent的做法,和以前Route的做法耗时差不多。
    而目前Fluent的Route做法,比以前Route节省50%-60%。

  50. Ivony...
    *.*.*.*
    链接

    Ivony... 2009-12-02 19:48:00

    装配脑袋:想起当年和Ivony在CSDN上对一个小小的问题拼命优化,就为了那几毫秒的提升……



    最后我又把那几毫秒的提升舍去了。。。。

    那是我第一次意识到数组的性能如此的高。。。。

  51. 老赵
    admin
    链接

    老赵 2009-12-02 19:52:00

    @Ivony...
    到底啥问题呀?

  52. Ivony...
    *.*.*.*
    链接

    Ivony... 2009-12-02 20:18:00

    Jeffrey Zhao:
    @Ivony...
    到底啥问题呀?




    其实蛮常用的,是DataTable到Entity的映射。

  53. 幸存者
    *.*.*.*
    链接

    幸存者 2009-12-02 22:38:00

    RednaxelaFX:
    CLR的JIT在看到for (int i = 0; i < array.Length; i++) ...的时候会把边界检查外提的。但如果写成:

    int len = array.Length;
    for (int i = 0; i < len; i++) {
      // ...
    }

    你再看看…… =_=||||

    现在的CLR的JIT还不够强悍啊


    这个你确定么?我刚才反汇编看了两种情况下生成的汇编,发现循环体内的汇编代码几乎完全一样。而且因为i < array.Length多了一个mov指令,实际上性能要稍慢于i < len这种情况。

  54. 幸存者
    *.*.*.*
    链接

    幸存者 2009-12-02 22:47:00

    哎哎,看来我犯了一个低级错误,楼上的回复可以忽略了。

    另外我比较惊讶的是foreach的性能竟然略优于for,即使是对数组进行操作。

  55. RednaxelaFX
    *.*.*.*
    链接

    RednaxelaFX 2009-12-02 22:58:00

    @幸存者
    嗯,这是我在3.5SP1上的测试:

    using System;
    
    namespace ConsoleApplication {
        static class Program {
            static void Foo(int[] array) {
                for (int i = 0; i < array.Length; i++) {
                    array[i] = 2;
                }
            }
    
            static void Bar(int[] array) {
                var len = array.Length;
                for (var i = 0; i < len; i++) {
                    array[i] = 2;
                }
            }
    
            static void Main(string[] args) {
                var array = new int[10];
                Foo(array);
                Bar(array);
            }
        }
    }

    这是结果:
    ConsoleApplication.Program.Foo(Int32[])
    Begin 00a300a8, size 1b
    00A300A8 55               push        ebp
    00A300A9 8BEC             mov         ebp,esp
    00A300AB 33D2             xor         edx,edx
    00A300AD 8B4104           mov         eax,dword ptr [ecx+4]
    00A300B0 85C0             test        eax,eax
    00A300B2 7E0D             jle         00A300C1
    00A300B4 C744910802000000 mov         dword ptr [ecx+edx*4+8],2
    00A300BC 42               inc         edx
    00A300BD 3BC2             cmp         eax,edx
    00A300BF 7FF3             jg          00A300B4
    00A300C1 5D               pop         ebp
    00A300C2 C3               ret
    
    ConsoleApplication.Program.Bar(Int32[])
    Begin 00a300d8, size 2a
    00A300D8 55               push        ebp
    00A300D9 8BEC             mov         ebp,esp
    00A300DB 56               push        esi
    00A300DC 8B7104           mov         esi,dword ptr [ecx+4]
    00A300DF 33C0             xor         eax,eax
    00A300E1 85F6             test        esi,esi
    00A300E3 7E14             jle         00A300F9
    00A300E5 8B5104           mov         edx,dword ptr [ecx+4]
    00A300E8 3BC2             cmp         eax,edx
    00A300EA 7310             jae         00A300FC
    00A300EC C744810802000000 mov         dword ptr [ecx+eax*4+8],2
    00A300F4 40               inc         eax
    00A300F5 3BC6             cmp         eax,esi
    00A300F7 7CEF             jl          00A300E8
    00A300F9 5E               pop         esi
    00A300FA 5D               pop         ebp
    00A300FB C3               ret
    00A300FC E8E3C30267       call        67A5C4E4 (JitHelp: CORINFO_HELP_RNGCHKFAIL)
    00A30101 CC               int         3

    代码还是比较有区别的,对吧?注意到Bar()里的显式数组边界检查。

    如果操作的数组的元素类型是引用类型,那么生成的代码就几乎是一样的,因为CLR的JIT知道边界检查会在CORINFO_HELP_ARRADDR_ST里做,就不会再在外面生成冗余的边界检查代码了。

  56. llj098
    *.*.*.*
    链接

    llj098 2009-12-02 23:43:00

    Duron800:

    Ivony...:
    @RednaxelaFX


    其实我所迷惑的是,如果用for的话,不是每次都要检查边界么?但从测试中看不到这个性能的损失。。。。。


    我猜是像CLR VIA C#里说的被编译器给优化了吧。只检查一次。



    印象中,这里的应该是 JIT 的优化。

    《Effective c#》中也提到过。

  57. 老赵
    admin
    链接

    老赵 2009-12-02 23:49:00

    @幸存者
    数组和List<T>的foreach都有优化。
    我记得数组是优化成for形式的,至少不是通过Enumerator进行。
    而List<T>的Enumerator是非virtual的,性能高些。
    当然,如果把数组和List<T>都cast成IEnumerable就都不优化了。

  58. 四有青年
    *.*.*.*
    链接

    四有青年 2009-12-03 09:16:00

    哇,评论比文章更吸引人!

  59. YinPSoft[未注册用户]
    *.*.*.*
    链接

    YinPSoft[未注册用户] 2009-12-05 14:35:00

    对性能比较类的文章比较有兴趣,这样可以在实际开发中有目的地选择更合理的方法

  60. tianxd
    *.*.*.*
    链接

    tianxd 2010-01-06 15:43:00

    @RednaxelaFX

    RednaxelaFX:

    Ivony...:
    这么说来,由于数组的协变存在,引用类型的数组Array.Copy还不能是直接内存拷贝?因为要检查赋值兼容性?


    Array.Copy()里会对数组的MethodTable做检查,当目标数组的元素类型是源数组的元素类型的子类型,就会进入单个单个引用复制的模式,每复制一个都检查一次看看能否正确cast。



    单个单个引用模式怎么理解?能给个解释吗

  61. tianxd
    *.*.*.*
    链接

    tianxd 2010-01-06 16:45:00

    RednaxelaFX:
    CLR的JIT在看到for (int i = 0; i < array.Length; i++) ...的时候会把边界检查外提的。但如果写成:

    int len = array.Length;
    for (int i = 0; i < len; i++) {
      // ...
    }

    你再看看…… =_=||||

    现在的CLR的JIT还不够强悍啊



    好像Debug和Release模式下他们执行效率正好相反,在debug下for (int i = 0; i < len; i++)反而更快

  62. tianxd
    *.*.*.*
    链接

    tianxd 2010-01-06 16:46:00

    而在release下感觉不相上下

  63. 老赵
    admin
    链接

    老赵 2010-01-06 16:58:00

    @tianxd
    不过性能评测时debug模式总是忽略的。

  64. RednaxelaFX
    *.*.*.*
    链接

    RednaxelaFX 2010-01-09 21:53:00

    tianxd:
    @RednaxelaFX

    RednaxelaFX:

    Ivony...:
    这么说来,由于数组的协变存在,引用类型的数组Array.Copy还不能是直接内存拷贝?因为要检查赋值兼容性?


    Array.Copy()里会对数组的MethodTable做检查,当目标数组的元素类型是源数组的元素类型的子类型,就会进入单个单个引用复制的模式,每复制一个都检查一次看看能否正确cast。



    单个单个引用模式怎么理解?能给个解释吗


    不是"单个单个引用模式",是"单个单个引用复制的模式"。这个代码路径上的逻辑就跟你自己写for循环来复制引用类型的数组几乎一样。

  65. Visitor.Name
    74.125.158.*
    链接

    Visitor.Name 2012-04-25 18:31:58

    @RednaxelaFX: CLR的JIT在看到 for (int i = 0; i < array.Length; i++) ...的时候会把边界检查外提的。但如果写成:int len = array.Length; for (int i = 0; i < len; i++) { ... } 你再看看…… =_=|||| 现在的CLR的JIT还不够强悍啊

    result

    长度为1000的List和int[],分别测试,用CodeTimer,各跑100K次

    public void forIn()
    {
        for (int i = 0; i < list.Count; i++)
        {
            bus = list[i];
        }
    }
    
    public void forOut()
    {
        int len = list.Count;
        for (int i = 0; i < len; i++)
        {
            bus = list[i];
        }
    }
    
    public void forInArr()
    {
        for (int i = 0; i < arr.Length; i++)
        {
            bus = arr[i];
        }
    }
    
    public void forOutArr()
    {
        int len = arr.Length;
        for (int i = 0; i < len; i++)
        {
            bus = arr[i];
        }
    }
    

    好像刚好相反啊……

  66. passer
    211.143.101.*
    链接

    passer 2013-12-07 22:05:43

    奇怪了 我在x64 .net 4.5环境下结果差异很大

    One by one
        Time Elapsed:   7,646ms
        CPU Cycles:     24,369,276,996
        Gen 0:          0
        Gen 1:          0
        Gen 2:          0
    
    Int32[].CopyTo
        Time Elapsed:   449ms
        CPU Cycles:     1,433,776,204
        Gen 0:          0
        Gen 1:          0
        Gen 2:          0
    
    Array.Copy
        Time Elapsed:   456ms
        CPU Cycles:     1,455,204,536
        Gen 0:          0
        Gen 1:          0
        Gen 2:          0
    
    Buffer.BlockCopy
        Time Elapsed:   452ms
        CPU Cycles:     1,444,410,336
        Gen 0:          0
        Gen 1:          0
        Gen 2:          0
    

发表回复

登录 / 登录并记住我 ,登陆后便可删除或修改已发表的评论 (请注意保留评论内容)

昵称:(必填)

邮箱:(必填,仅用于Gavatar

主页:(可选)

评论内容(大于5个字符):

  1. Your Name yyyy-MM-dd HH:mm:ss

使用Live Messenger联系我