为什么第二个for循环总是比第一个循环执行得快?

时间:2009-06-20 14:35:28

标签: c# winforms performance diagnostics

我试图找出for循环是否比foreach循环更快并且使用System.Diagnostics类来计算任务的时间。在运行测试时,我注意到我先放入的循环始终比最后一循环执行得慢。有人可以告诉我为什么会这样吗?我的代码如下:

using System;
using System.Diagnostics;

namespace cool {
    class Program {
        static void Main(string[] args) {
            int[] x = new int[] { 3, 6, 9, 12 };
            int[] y = new int[] { 3, 6, 9, 12 };

            DateTime startTime = DateTime.Now;
            for (int i = 0; i < 4; i++) {
                Console.WriteLine(x[i]);
            }
            TimeSpan elapsedTime = DateTime.Now - startTime;

            DateTime startTime2 = DateTime.Now;
            foreach (var item in y) {
                Console.WriteLine(item);
            }
            TimeSpan elapsedTime2 = DateTime.Now - startTime2;

            Console.WriteLine("\nSummary");
            Console.WriteLine("--------------------------\n");
            Console.WriteLine("for:\t{0}\nforeach:\t{1}", elapsedTime, elapsedTime2);

            Console.ReadKey();
      }
   }
}

以下是输出:

for:            00:00:00.0175781
foreach:        00:00:00.0009766

8 个答案:

答案 0 :(得分:16)

可能是因为类(例如Console)需要第一次进行JIT编译。您将通过首先调用所有方法(JIT(温暖然后)),然后执行测试来获得最佳指标。

正如其他用户所指出的那样,4次传球永远不足以向您展示差异。

顺便说一句,for和foreach之间的性能差异可以忽略不计,使用foreach的可读性好处几乎总是超过任何边际性能优势。

答案 1 :(得分:7)

  1. 我不会使用DateTime来衡量效果 - 尝试Stopwatch类。
  2. 只用4次测量就不会给你带来好成绩。更好地使用&gt; 100.000次传递(你可以使用外环)。请勿在循环中执行Console.WriteLine
  3. 更好:使用分析器(如Redgate ANTS或NProf)

答案 2 :(得分:3)

我不是在C#中,但是当我没记错的时候,微软正在构建Java的“及时”编译器。当他们在C#中使用相同或类似的技术时,“某些结构在第二时间表现得更快”将是相当自然的。

例如,可能是JIT-System看到循环被执行并决定adhoc来编译整个方法。因此,当到达第二个循环时,它仍然被编译并且比第一个循环执行得快得多。但这是对我的一个相当简单的猜测。当然,您需要在C#运行时系统中有更深入的了解才能了解正在发生的事情。也可能是,RAM-Page首先在第一个循环中被访问,而在第二个循环中它仍然在CPU缓存中。

Addon:另外发表的评论:输出模块可以在第一次循环中第一次被JIT,而不是我的第一次猜测。现代语言非常复杂,无法找到引擎盖下的内容。我的这个陈述也符合这个猜测:

但你的循环中也有终端输出。他们让事情变得更加困难。也可能是,在程序中第一次打开终端需要一些时间。

答案 3 :(得分:3)

我只是在进行测试以获得一些真实数字,但与此同时Gaz打败了我的答案 - 第一次调用时调用Console.Writeline,所以你在第一次循环中支付了这笔费用。

仅供参考 - 使用秒表而不是日期时间和测量滴答数:

在第一次循环之前没有调用Console.Writeline,时间是

for: 16802
foreach: 2282

通过调用Console.Writeline,他们

for: 2729
foreach: 2268

虽然这些结果由于运行次数有限而不能始终如一地重复,但差异的大小总是大致相同。


编辑后的代码:

        int[] x = new int[] { 3, 6, 9, 12 };
        int[] y = new int[] { 3, 6, 9, 12 };

        Console.WriteLine("Hello World");

        Stopwatch sw = new Stopwatch();

        sw.Start();
        for (int i = 0; i < 4; i++)
        {
            Console.WriteLine(x[i]);
        }
        sw.Stop();
        long elapsedTime = sw.ElapsedTicks;

        sw.Reset();
        sw.Start();
        foreach (var item in y)
        {
            Console.WriteLine(item);
        }
        sw.Stop();
        long elapsedTime2 = sw.ElapsedTicks;

        Console.WriteLine("\nSummary");
        Console.WriteLine("--------------------------\n");
        Console.WriteLine("for:\t{0}\nforeach:\t{1}", elapsedTime, elapsedTime2);

        Console.ReadKey();

答案 4 :(得分:2)

为什么foreach版本中有几种形式的开销在for循环中不存在

  • 使用IDisposable。
  • 每个元素的附加方法调用。必须使用IEnumerator<T>.Current(方法调用)在引擎盖下访问每个元素。因为它在界面上,所以无法内联。这意味着N方法调用,其中N是枚举中元素的数量。 for循环只使用和索引器
  • 在foreach循环中,所有调用都通过一个接口。一般来说,这比通过具体类型
  • 慢一点

请注意,我上面列出的内容一定是巨额费用。它们通常是非常小的成本,可以导致小的性能差异。

另请注意,正如Mehrdad指出的那样,编译器和JIT可能会选择针对某些已知数据结构(如数组)优化foreach循环。最终结果可能只是一个for循环。

注意:您的性能基准测试通常需要更多准确的工作。

  • 您应该使用StopWatch而不是DateTime。它对性能基准测试更加准确。
  • 您应该多次执行测试
  • 你需要在每个循环上进行虚拟运行,以消除第一次使用JITing方法时出现的问题。当所有代码采用相同的方法但不会造成伤害时,这可能不是问题。
  • 您需要在列表中使用多于4个值。尝试40,000而不是。

答案 5 :(得分:1)

您应该使用StopWatch来计算行为时间。

从技术上讲, for 循环更快。 Foreach 在IEnumerable的迭代器上调用MoveNext()方法(从调用创建方法堆栈和其他开销),当 for 只需要增加变量时。

答案 6 :(得分:1)

我不明白为什么这里的每个人都说在这种特殊情况下for会比foreach更快。对于List<T>,它(通过列表的foreach比通过for的{​​{1}}慢约2倍。)

事实上,List<T>会比foreach稍快 。因为数组上的for基本上编译为:

foreach

使用for(int i = 0; i < array.Length; i++) { } 作为停止条件允许JIT删除对阵列访问的边界检查,因为这是一种特殊情况。使用.Length使JIT插入额外的指令来检查每次迭代i < 4是否超出数组的范围,如果是这种情况则抛出异常。但是,对于i,它可以保证您永远不会超出数组边界,因此边界检查是多余的,使其更快。

然而,在大多数循环中,与内部工作相比,循环的开销是微不足道的。

您所看到的差异只能通过JIT解释。

答案 7 :(得分:1)

我不会读太多 - 由于以下原因,这不是很好的分析代码 1. DateTime不适用于分析。您应该使用使用CPU硬件配置文件计数器的QueryPerformanceCounter或StopWatch 2. Console.WriteLine是一种设备方法,因此可能会有微妙的影响,如缓冲,以考虑到 3.运行每个代码块的一次迭代永远不会给你准确的结果,因为你的CPU在飞行优化中做了很多时髦,例如乱序执行和指令调度
4.有可能两个代码块的JITed代码非常相似,因此很可能在第二个代码块的指令缓存中

为了更好地了解时间,我做了以下

  1. 用数学表达式(e ^ num)
  2. 替换Console.WriteLine
  3. 我使用QueryPerformanceCounter / QueryPerformanceTimer通过P / Invoke
  4. 我运行了每个代码块100万次然后平均结果
  5. 当我这样做时,我得到了以下结果:

    for循环耗时0.000676毫秒 foreach循环花了0.000653毫秒

    所以foreach的速度非常快,但不是很多

    然后我做了一些进一步的实验并先运行foreach块,然后运行第二块 当我这样做时,我得到了以下结果:

    foreach循环耗时0.000702毫秒 for循环花了0.000691毫秒

    最后,我将两个循环一起运行两次,即+ foreach然后再次+ foreach 当我这样做时,我得到了以下结果:

    foreach循环耗时0.00140毫秒 for循环耗时0.001385毫秒
    所以基本上它在我看来,无论你运行的代码是什么,运行速度都要快一些,但不是 足以具有任何意义。
    - 编辑 -
    这里有几个有用的链接
    How to time managed code using QueryPerformanceCounter
    The instruction cache
    Out of order execution