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);
}
(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和越界混在一起,将是软件界的一大笑话。
其实那个测试本身就是一个错误的测试。
为什么这样说呢?让我们来仔细分析,为求简单,只分析循环体内为空的测试。
原测试代码:
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];
}
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)
{
}
编译器你去判断去吧。
勿在浮沙筑高塔。
版权所有,欢迎转载