for与foreach再探讨

        飞林沙 在《for和foreach,N个人N种结论的选择》一文中通过例子测试了 for与foreach的性能。这篇文章重点探讨下他那个测试本身。

        其实那个测试本身就是一个错误的测试。

        为什么这样说呢?让我们来仔细分析,为求简单,只分析循环体内为空的测试。

 

        原测试代码:



       
static void ForTest()
        {
            
long length = 40000000;
            Stopwatch thisStopwatch 
= new Stopwatch();

            
long[] i = new long[length];
            
for (long j = 0; j < length; j++)
            {
                i[j] 
= j;    
            }

            thisStopwatch.Start();
            
for (long j = 0; j < i.Length; ++j)
            {
            }

            thisStopwatch.Stop();
            Console.WriteLine(thisStopwatch.Elapsed.Milliseconds);
        }
        
        
static void ForeachTest()
        {
            
long length = 40000000;
            Stopwatch thisStopwatch1 
= new Stopwatch();
            
long[] i = new long[length];
            
for (long j = 0; j < length; j++)
            {
                i[j] 
= j;
            }
            thisStopwatch1.Start();
            
foreach (long j in i)
            {
            }
            thisStopwatch1.Stop();
            
            Console.WriteLine(thisStopwatch1.Elapsed.Milliseconds);
        }


        这个测试代码和飞林沙的原文比,略有变动。主要是去掉了length的const。
        
        我的测试结果:for -- 147    foreach -- 82
        
        直接将i.Length替换成length,测试结果:for -- 88 foreach -- 82
        
        就这两个测试而言,貌似可以得出这样一个结论:
        
        (1)只读式集合foreach比for快
        (2)在循环体中使用i.Length比使用length要慢很多
        
        实际上这两个结论都是错误的。错误的原因,在于这个测试本身就是一个不公平的测试。对for很不公平。
        
        用reflector反编译。

        (a)
            
for (long j = 0; j < i.Length; ++j)
            {
            }
            
=>
            
for (long j = 0L; j < numArray2.Length; j += 1L)
            {
            }
            
        (b)
            
for (long j = 0; j < length; ++j)
            {
            }
            
=>
            
for (long j = 0L; j < 0x2625a00L; j += 1L)
            {
            }
            
        (c)
            
foreach (long j in i)
            {
            }
            
=>
            
for (int j = 0; j < numArray2.Length; j++)
            {
                
long num1 = numArray2[j];
            }

 

        可以看出,这里影响性能的关键是 Int32和Int64 的差别。
        
        (a)中主要是Int64的计算,还涉及到一个Int32 (numArray2.Length)到Int64的转变。
        (b)中主要是Int64的计算,不过不涉及到Int32到Int64的转变,因此比(a)快得多。
        (c)中主要是Int32的计算,其中也涉及到一个Int32到Int64的转变。至于(c)比(b)略快,这里可以看出,对于这个例子而言,编译器将foreach转变成了for循环。(c)和(b)的性能差别,其实是Int32计算+Int32->Int64计算和纯Int64计算的差别。
        
        从(c)中也可以看见编译器傻X的一面。明明j没被调用,它还要偏偏的将它转化出来。
        
        所以说,飞林沙的测试是个错误的测试。为使公平起见,需要将测试代码中的long全部改成Int32。
        
        测试结果:
        
        for中使用i.Length: for -- 22, foreach -- 52
        for中不使用i.Length: for -- 21, foreach -- 52
        
        为什么 foreach 比 for 慢这么多呢?原因上面说过,编译器太傻X了,在循环体中加了条语句:int num1 = numArray2[j];

        下面谈一谈编译器的优化问题。
        
        我们和编译器相比,各自优势是什么?
        
        我们比编译器更清楚我们的代码是干什么用的,但在代码的执行原理上比不上编译器(其实是比不上写编译器的那些人),比如,《for和foreach,N个人N种结论的选择》原文作者就没考虑到Int32和Int64的问题。当然,牛人牛到一定程度,比写编译器的人还牛,也是很有可能的。
        
        编译器比我们更清楚代码的执行原理,但是它不知道我们这行代码是干什么用的。它的优化是有前提的,第一点是不能出错误,第二点是优化的通用性。另外一点,编译器的聪明也是有限的。这几点导致编译器的优化绝大部分情况下不是最佳的优化。
        
        有很多优化策略确实可以将程序性能提高很多,但是如果不能保证程序的正确性,编译器倾向于不做这样的优化。而人工写代码,可以在代码中或者在应用中确保这种程序错误不会出现,就可以手动的选择这种优化策略。关于编译器优化,《深入理解计算机系统》一书 第5章 优化程序性能 讲的很精彩。

        所以我认为,一般情况下foreach是比for效率低一点的,只要for循环写的不是太差。
        
        针对 《for和foreach,N个人N种结论的选择》 文后的回复,我觉得有几个错误的看法需要指出。
        
        (1)所谓 for 有条件检查,foreach不要条件检查
        
        诚然,for每一促循环都要判断循环条件是否成立,难道foreach中每一次迭代都不需要判断当前迭代器的位置是否到头?
        
        (2)判断越界问题
        
        判断索引的越界,在越界时抛出异常,是容器的职责,而不是调用它的代码的职责。这一问题和for和foreach都无关系。在语义上,for与foreach并不保证不越界。这是最基本的职责分离原则,微软如果把for, foreach和越界混在一起,将是软件界的一大笑话。
        

        很简单的一个例子:

        for(int i = 1000; i<(myarray.Length)*2;i=i+2)
        {

        }

 

        编译器你去判断去吧。
        
        勿在浮沙筑高塔。

 

posted @ 2008-08-24 00:40  xiaotie  阅读(5132)  评论(7编辑  收藏  举报