6

我试图弄清楚 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
4

8 回答 8

16

可能是因为类(例如控制台)需要在第一次通过时进行 JIT 编译。通过首先调用所有方法(对它们进行 JIT(热身然后预热)),然后执行测试,您将获得最佳指标。

正如其他用户所指出的,4 次通行证永远不足以向您展示差异。

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

于 2009-06-20T14:45:51.650 回答
7
  1. 我不会使用 DateTime 来衡量性能 - 试试这个Stopwatch课程。
  2. 仅通过 4 次测量永远不会给您带来好的结果。更好地使用 > 100.000 次传球(您可以使用外循环)。不要Console.WriteLine在你的循环中做。
  3. 更好的是:使用分析器(如 Redgate ANTS 或 NProf)
于 2009-06-20T14:42:57.117 回答
3

我不太熟悉 C#,但当我没记错的时候,微软正在为 Java 构建“即时”编译器。当他们在 C# 中使用相同或相似的技术时,“一些次之的构造执行得更快”是很自然的。

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

插件:另一个评论是:输出模块可以在第一个循环中第一次 JITed 对我来说比我的第一个猜测更有可能。现代语言只是非常复杂地找出引擎盖下做了什么。我的这个陈述也符合这个猜测:

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

于 2009-06-20T14:47:28.183 回答
3

我只是在执行测试以获得一些实数,但与此同时,Gaz 击败了我的答案——对 Console.Writeline 的调用在第一次调用时被忽略,所以你在第一个循环中支付了这笔费用。

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

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

为:16802
前锋:2282

通过调用 Console.Writeline 他们是

为:2729
前锋: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();
于 2009-06-20T14:57:31.423 回答
2

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

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

请注意,我上面列出的东西不一定是巨大的成本。它们通常是非常小的成本,可能会导致很小的性能差异。

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

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

  • 您应该使用 StopWatch 而不是 DateTime。对于性能基准,它要准确得多。
  • 您应该多次执行测试,而不仅仅是一次
  • 您需要在每个循环上进行一次虚拟运行,以消除第一次 JITing 方法带来的问题。当所有代码都在同一个方法中时,这可能不是问题,但它不会造成伤害。
  • 您需要在列表中使用超过 4 个值。试试 40,000。
于 2009-06-20T14:40:32.113 回答
1

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

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

于 2009-06-20T14:46:43.630 回答
1

我不明白为什么这里的每个人都说这for会比foreach这种特殊情况更快。对于 a List<T>,它是(foreach通过 List 比for通过 a慢大约 2 倍List<T>)。

事实上,foreach会比这里稍微for一点。因为foreach在数组上基本上编译为:

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

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

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

您看到的差异只能由我猜的 JIT 来解释。

于 2009-06-20T15:09:15.050 回答
1

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

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

  1. 将 Console.WriteLine 替换为数学表达式 (e^num)
  2. 我通过 P/Invoke 使用了 QueryPerformanceCounter/QueryPerformanceTimer
  3. 我将每个代码块运行 100 万次,然后将结果取平均值

当我这样做时,我得到了以下结果:

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

所以 foreach 稍微快一点,但不是很多

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

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

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

foreach 循环花费了 0.00140 毫秒
for 循环花费了 0.001385 毫秒

所以基本上在我看来,无论您第二次运行什么代码,运行速度都会稍微快一些,但不足以产生任何意义。
--Edit--
这里有几个有用的链接
How to time managed code using QueryPerformanceCounter
指令缓存乱
序执行

于 2009-06-20T15:20:13.407 回答