之前一直认为
for (int i = 0, h = arr.Count; i < h; i++)

for (int i = 0; i < arr.Count; i++)
两种写法,在C#里应该是差不多的,今天突然有人问,就写了个程序测试了一下,结果出乎我的意料
如果arr是List<T>,前者的效率比后者高大约一倍,如果arr是string[],两者效率基本差不多测试代码:
int tnum = 100000; // 添加或查找的次数
int outnum = 10; // 外层循环次数List<string> arr = new List<string>();
for(int i=0;i<tnum;i++)
    arr.Add(i.ToString());
string[] arr2 = new string[tnum];for(int j=0;j<outnum;j++)
{
    Stopwatch watch = new Stopwatch();
    string msg;    msg = "Number ";
    watch.Reset();
    watch.Start();
    for (int i = 0, h = arr.Count; i < h; i++)
    {
    }
    watch.Stop();
    Console.WriteLine(msg + "耗时:" + watch.ElapsedTicks.ToString());    msg = ".Count ";
    watch.Reset();
    watch.Start();
    for (int i = 0; i < arr.Count; i++)
    {
    }
    watch.Stop();
    Console.WriteLine(msg + "耗时:" + watch.ElapsedTicks.ToString());    msg = "Length ";
    watch.Reset();
    watch.Start();
    for (int i = 0; i < arr2.Length; i++)
    {
    }
    watch.Stop();
    Console.WriteLine(msg + "耗时:" + watch.ElapsedTicks.ToString());
}

解决方案 »

  1.   

    发这个帖的意思是,要养成良好的代码习惯,
    当然,这个不是影响代码性能的重点,只是平时写代码时,顺手就写上效率较高的代码,会更好。类似的提升效率的地方,还有拼接字符串时,值类型一定要加上ToString()
    比如用:string a = "aa" + 123.ToString();
    而不是用string a = "aa" + 123
      

  2.   

    再比如:
    字符串比较或查找,请加上StringComparison.Ordinal参数如果要忽略大小写,请使用StringComparison.OrdinalIgnoreCase
    比如要使用:str.IndexOf("abc", StringComparison.Ordinal)
    而不是使用:str.IndexOf("abc"),这个等于str.IndexOf(value,StringComparison.CurrentCulture)
    StringComparison.CurrentCulture:使用区域敏感排序规则和当前区域比较字符串
    StringComparison.Ordinal:使用序号排序规则比较字符串
      

  3.   

    这很正常啊、
    即使arr是string[]、也会差很多的
    一般for中的第二个条件一般都是2个变量、
    我很少用不是变量的数存进去
      

  4.   

    错,你看我的测试代码,String[]是差不多的
    List的Count,代码是return this._size;估计时间是消耗在对象引用查找上
      

  5.   

    再帖一个好习惯:
    HashTable、Dictionary、SortedList、SortedDictionary等字典使用 a、使用字典的TryGetValue方法,如:
    Dictionary<string, string> abc;
    string a;
    if(!abc.TryGetValue(key, out a)){
        //key不存在
    }else{
    }
    而不要用下面的代码,因为下面的代码重复查找了2次key:
    if(!abc.ContainKeys(key))
    {
        //key不存在
    }else{
        a = abc[key];
    }
    b、删除字典的key时,直接使用Remove方法,不要事先判断,比如:
    Dictionary<string, string> abc;
    if(abc.Remove(key)){// 没必要先判断ContainKeys,重复查找,浪费性能
        //key存在,且移除成功
    }else{
        //key不存在,或移除失败
    }
    c、插入元素时,直接使用this[key] = value,如:
    abc[key] = value;// 注意需求,如果允许覆盖才可以用
    而不需要:if(!abc.ContainsKey(key))abc.Add(key, value);
    反编译代码,可以看到Add和this[]是调用同一个方法的
      

  6.   

    Count 那种费时,是因为每次访问arr.Count都要去取出这个arr.Count,汇编要进行好几次的交换,而且number那种,取出count后放到h中,以后直接比较h立即数即可
      

  7.   

    对list来说这两种写法使用的场合不同,前者不允许删减list,后者可以。
      

  8.   

    for (int i = 0, h = arr.Count; i < h; i++)
    相当于
    h = arr.Count;
    for (int i = 0; i < h; i++)
      

  9.   

    看了一下il代码,
    for (int i = 0, h = arr.Count; i < h; i++)
      IL_0054:  ldc.i4.0
      IL_0055:  stloc.s    V_8
      IL_0057:  ldloc.2
      IL_0058:  callvirt   instance int32 class [mscorlib]System.Collections.Generic.List`1<string>::get_Count()
      IL_005d:  stloc.s    h
      IL_005f:  br.s       IL_0067
      IL_0061:  ldloc.s    V_8
      IL_0063:  ldc.i4.1
      IL_0064:  add
      IL_0065:  stloc.s    V_8
      IL_0067:  ldloc.s    V_8
      IL_0069:  ldloc.s    h
      IL_006b:  blt.s      IL_0061  
      
    for (int i = 0; i < arr2.Length; i++)
      IL_00aa:  ldc.i4.0
      IL_00ab:  stloc.s    V_10
      IL_00ad:  br.s       IL_00b5
      IL_00af:  ldloc.s    V_10
      IL_00b1:  ldc.i4.1
      IL_00b2:  add
      IL_00b3:  stloc.s    V_10
      IL_00b5:  ldloc.s    V_10
      IL_00b7:  ldloc.s    arr2
      IL_00b9:  ldlen
      IL_00ba:  conv.i4
      IL_00bb:  blt.s      IL_00af
      
    for (int i = 0; i < arr.Count; i++)
      IL_00fa:  ldc.i4.0
      IL_00fb:  stloc.s    V_11
      IL_00fd:  br.s       IL_0105
      IL_00ff:  ldloc.s    V_11
      IL_0101:  ldc.i4.1
      IL_0102:  add
      IL_0103:  stloc.s    V_11
      IL_0105:  ldloc.s    V_11
      IL_0107:  ldloc.2
      IL_0108:  callvirt   instance int32 class [mscorlib]System.Collections.Generic.List`1<string>::get_Count()
      IL_010d:  blt.s      IL_00ff
    时间应该就是花在i < arr.Count里,每次循环都去调用get_Count()方法了,
    难道是这一步要取对象引用,再获取对象的方法返回值导致的耗时吗,呵呵
      

  10.   

    最近也有这样感觉,有时不能为了少写代码却消耗CPU资源,所以,写好程序后多看几遍,优化是有必要的。
    如原来为了省事,复制就算了:
    if (指定.IndexOf(转换.Substring(字符, 1)) >= 0) 等效 += 指定.Substring(指定.IndexOf(转换.Substring(字符, 1)), 1);
    显然上面这样写是消耗的写法,是不尽合理的,应该优化如下:
    int 位置 = 指定内容.IndexOf(转换内容.Substring(字符, 1));
    if (位置 >= 0) 等效 += 指定.Substring(位置, 1);
    只要不重复执行同样转换应该就可以了。
      

  11.   

    个人觉得单独提取出来h=arr.length比较好 不然代码怎么看怎么不舒服~
    主要时间还是浪费在每次循环都需要调用get方法吧 
      

  12.   

    Release版本,启动调试和开始执行(不调试)的结果不同,在后一种情况下,三种写法的计数相近,差别消失
    最终还是被JIT优化了??
      

  13.   

    按SP老大的说法,这种循环几十W次然后得出性能结论的做法都是蛋疼的
      

  14.   


    请教一下il代码怎么显示出来的,还有如何去阅读?
    上次我想在.net里面找这个功能的,没有找到
      

  15.   

    忽然发现这个东西在more effective c#里面已经写了~
      

  16.   

    实测无任何差别。
        static void Main(string[] args)
            {  
                List<int> li = new List<int>(1000000);
                for (int i = 0; i < li.Capacity; i++)
                {
                    li.Add(i);
                }
                DateTime d1 = DateTime.Now;
                //for (int i = 0; i < li.Count; i++)
                //{
                //    li[i] = li[i] * 100;
                //}
                for (int i = 0, h = li.Count; i < h; i++)
                {
                    li[i] = li[i] * 100;
                }
                DateTime d2 = DateTime.Now;
                Console.WriteLine(d2-d1);
                Console.Read();
            }
      

  17.   

    这两种实际上是没有区别的。
    因为count函数会被优化成直接访问内存。
    而cpu有cache,最终这种访问内存的操作都会变成访问cache。
    所以无论是哪一种,实际上是没有多少差别的。
      

  18.   

    确实,release版本的没有区别。
      

  19.   

    谢谢LZ,关注细节,受教。
    我写的时候一般都是
    int i,arr.count=N;
    for(i=0;i<arr.count;i++)
    今天学习了
      

  20.   

    Clr via C# 里面已经有结论了啊,数组两个没太多区别,因为后者会优化为前者
    至于泛型则不优化,前者肯定优于后者..
      

  21.   

    for语句的效率,可以把它看成硬件处理速率的比较
      

  22.   


    就是耗在i<arr.Count,每次要去取引用,而不是直接取值。
      

  23.   


    问题出在arr.Count。。
      

  24.   

    其实吧,不应该抛开循环体来测效率。我觉得一般循环中操作耗时远远大于循环本身,所以可以这么来看:for (int i = 0; i < arr.Count; i++){
        // 空操作
    }根据测试结果,10 000 000 次循环如下:(时钟周期)Number 耗时:114359
    .Count 耗时:166333
    .Length 耗时:108633
    Number 耗时:122153
    .Count 耗时:164890
    .Length 耗时:111406表面上看,array.Count最高(对比.Length)浪费约 53% 效率,似乎很高,但实际上呢?来看看花费的时间:(毫秒)
    Number 耗时:41
    .Count 耗时:48
    .Length 耗时:32
    Number 耗时:33
    .Count 耗时:46
    .Length 耗时:29也就是说即使是最慢的array.Count,平均单次花费时间最高也不超过 0.4us (约0.36us),如果按照循环体中操作耗时 10ms 计算(比较简单的循环体),array.Count浪费的时间相当于 0.004% 效率,就是说即便用了看起来最烂的array.Count方法,循环体仍最少保有原来的 99.996% 的效率。循环体内部耗时越长,array.Count浪费的效率越可以忽略不计所以在计算量小循环次数多的for中考虑这些就好了,也不必太在意,对于一般应用,哪种都一样。
      

  25.   


    两个都可以删减list,比如说前者:
    for (int i = 0, h = arr.Count; i < h; i++)
    如果你写成这样,就能删减list:
    for (int h = arr.Count, int i = h-1, ; i >= 0; i--)
      

  26.   

    zhao4zhong1 "不要迷信书,不要迷信权威......"要看汇编代码,而不是IL中间码。 要看release,不要看debug
    表头
    不同的循环写法
    1.
    2.
    3.另外,大家也看得出, 广告鼓吹的即时优化,还是比较xx的。 release的空循环优化,这么基本的编译器优化,都做不到。 (上面的是 x86 release的代码。  x64的release代码,也类似)
      

  27.   

    图,没显示完全。右键,查看 大图 即可(或者空间相册), 看到 对应的 三种循环写法的 对应的汇编( Intel Nehalem 架构的 x86 release 版本的汇编)
      

  28.   

    大大地脱离了底层, 成了盲人摸象。不得不抱怨一下微软,还有 青鸟。 (前者的降低了编程人员所需的知识,后者培训出大量不合格的计算机编程人员)对于循环1 和 循环2 
    for (int i = 0, h = arr.Count; i < h; i++)
    for (int i = 0; i < arr.Count; i++)     
    两者的 release代码,没区别, 都是 清空计数器edx(i=0)
    把array 大小 move到eax。   都是 cmp edx, eax  比较两者,然后 jl (jump less)
    第三种写法,也没什么差别。
    总得说,三种写法都没差别。这些东西,在c/c++的人心中,都是很清楚,没有争议的。
    如果, 判断条件的  arr.count 在循环体内,是有变化的。有变动的,那么, 必须在for的第二个条件,判断。
    如果循环的count,循环体内没变动,那么,都是 直接成为一个常量,在 eax 里面。 (不管你用何种写法,独立变量,或是 循环判断句内)
      

  29.   

    sp1234老大说:这种循环几十W次然后得出性能结论的做法都是蛋疼的
    我也蛋疼一次
    楼上有几位从IL和汇编分析,我没他们那么蛋疼
    我只从字段和属性分析
    属性在IL层面,分变成函数
    调用函数会不会比调用字段更快呢?
    不得而知
    试试千万次的蛋疼循环吧using System;
    using System.Collections.Generic;
    using System.Diagnostics;namespace ConsoleApplication1
    {
        public class Program
        {
            static void Main()
            {
                int tnum = 10000000; // 添加或查找的次数
                int outnum = 10; // 外层循环次数
                int iTmp = 0;            List<TestClass> arr = new List<TestClass>(tnum);
                for (int i = 0; i < tnum; ++i)
                {
                    arr.Add(new TestClass());
                }            for (int j = 0; j < outnum; j++)
                {
                    Stopwatch watch = new Stopwatch();
                    string msg;                msg = "属性赋值 ";
                    watch.Reset();
                    watch.Start();
                    for (int i = 0; i < tnum; ++i)
                    {
                        arr[i].Property = i;
                    }
                    watch.Stop();
                    Console.WriteLine(msg + "耗时:" + watch.ElapsedTicks.ToString());                msg = "字段赋值 ";
                    watch.Reset();
                    watch.Start();
                    for (int i = 0; i < tnum; ++i)
                    {
                        arr[i].Field = i;
                    }
                    watch.Stop();
                    Console.WriteLine(msg + "耗时:" + watch.ElapsedTicks.ToString());                msg = "属性取值 ";
                    watch.Reset();
                    watch.Start();
                    for (int i = 0; i < tnum; ++i)
                    {
                        iTmp = arr[i].Property;
                    }
                    watch.Stop();
                    Console.WriteLine(msg + "耗时:" + watch.ElapsedTicks.ToString());                msg = "字段取值 ";
                    watch.Reset();
                    watch.Start();
                    for (int i = 0; i < tnum; ++i)
                    {
                        iTmp = arr[i].Field;
                    }
                    watch.Stop();
                    Console.WriteLine(msg + "耗时:" + watch.ElapsedTicks.ToString() + "\r\n");
                }            Console.ReadKey();
            }
        }    public class TestClass
        {
            public Int32 Field;
            public Int32 Property
            {
                get { return Field; }
                set { Field = value; }
            }
        }
    }
      

  30.   


    你忽略了一点,就是编程的目的,你说微软降低了编程人员所需的知识,我觉得应该换一句话,那就是微软让编程人员把时间花在了真正需要的地方--软件要实现的目标,我只是要写个酒店管理系统或者为测试人员编写测试工具,或者方便我自己日常生活的一些小工具,我根本没必要去了解这些东西在底层是如何实现的,你觉得用PS的必须要清除PS中每个功能是如何实现的吗?根本不需要,他们只需要了解这些功能的用途,能够熟练的把他们组合在一起做出漂亮的图就可以了
      

  31.   

    如果我要写安全类软件,我就当然要去了解底层,否则我连最简单的hook都不知道应该hook谁,应该注意些什么,所以还是那句话,你干什么活,就去了解什么,计算机的知识太广了,不可能都精通,不同的语言,不同的工具都有其面向的重点,还有,我并不觉得写驱动的就比写界面的要高深复杂多少,分工不同而已
      

  32.   

    CandPointer说得不错
    我们首要注意的是发布给客户的最终执行的机器代码是经过csc和JIT两次优化的,因此,要确定我们的代码的确有更高的实际运行效率,不仅要选择release版本,而且程序不能由调试器启动(调试模式),而是要直接运行
    我个人也对运行效率有一种偏执的追求,但很多认为高效率的写法,最终多半发现其实是白费那个劲了
    两个相关的链接:
    http://technet.microsoft.com/zh-cn/query/ms241594
    http://www.cnblogs.com/neoragex2002/archive/2006/12/25/sampleofjitoptimization.html
      

  33.   

    如果arr是List<T>,前者的效率比后者高大约一倍,如果arr是string[],两者效率基本差不多
    ===========================================
    List<T>应该是用链表来实现的吧,计算元素总数要从头到尾遍历一次,而string[]是数组,能直接得出元素个数。
    这些都是数据结构中最基本的东西啊。
      

  34.   

    我没测试Release版本,但是养成这样的代码习惯还是有必要的
    因为Javascript没有Release版本,而在Javascript里,是每次都要重新计算length,性能低下
      

  35.   

    你错了,List内部有个_size记录容量,并没有去从头遍历一次
    你应该先看反编译后的代码,再来回答的
      

  36.   

    很多人的评价,都从C/C++的高度,来bs我们这些做web开发的
    也许你们忘记了,现在是web的时代,很多人都不只是写服务端代码,也要写很多的客户端脚本,
    如果你没有养成一个良好的代码习惯,认为
    for (int i = 0; i < arr.Count; i++)
    这个写法没有问题,那么你的js代码效率可想而知当然你可以说,我在服务端用这种写法,在js用另外一种,可能你记忆非常好,能做到,但我相信
    一半以上像我一样的码农是做不到的我经常在js代码里写:
    int a=123;
    然后运行时出错,查错查半天,才发现是用var a=123;所以,我又重申一下我的建议:养成良好的代码习惯,不是可能出问题的代码习惯