首页 文章

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

提问于
浏览
5

我试图弄清楚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();
      }
   }
}

Here is the output:

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

8 回答

  • 16

    我不是很喜欢C#,但是当我没记错的时候,微软正在构建Java的“Just in Time”编译器 . 当他们在C#中使用相同或类似的技术时,“第二次执行的某些结构执行得更快”是相当自然的 .

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

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

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

  • 3
    • 我不会使用DateTime来衡量性能 - 试试 Stopwatch 类 .

    • 仅用4次传球进行测量永远不会给你带来好成绩 . 更好地使用> 100.000次传递(您可以使用外部循环) . 不要在循环中执行 Console.WriteLine .

    • 更好:使用分析器(如Redgate ANTS或NProf)

  • 1

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

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

  • 2

    我只是在进行测试以获得一些真实的数字,但在此期间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();
    
  • 7

    由于以下原因,我不会提供良好的性能分析代码

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

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

    • 用数学表达式替换Console.WriteLine(e ^ num)

    • 我通过P / Invoke使用了QueryPerformanceCounter / QueryPerformanceTimer

    • 我运行了每个代码块100万次,然后平均结果

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

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

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

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

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

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

    foreach循环耗时0.00140毫秒
    for循环花了0.001385毫秒

    因此,基本上我认为无论你运行的代码是什么,运行速度都要快得多,但不足以发挥任何意义 .

  • 1

    之所以在foreach版本中有几种形式的开销在for循环中不存在

    • 使用IDisposable .

    • 每个元素的附加方法调用 . 必须使用 IEnumerator<T>.Current (方法调用)在引擎盖下访问每个元素 . 因为它在界面上无法内联 . 这意味着N方法调用,其中N是枚举中元素的数量 . for循环只使用和索引器

    • 在foreach循环中,所有调用都通过一个接口 . 一般来说这比通过具体类型慢一点

    请注意,我上面列出的东西是 not 必然是巨大的成本 . 它们通常是非常小的成本,可以导致小的性能差异 .

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

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

    • 您应该使用StopWatch而不是DateTime . 它对性能基准测试更加准确 .

    • 您应该多次执行测试而不是一次

    • 您需要在每个循环上执行虚拟运行,以消除第一次JITing方法时出现的问题 . 这可能不会受到伤害 .

    • 您需要在列表中使用多于4个值 . 请尝试40,000 .

  • 3

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

    事实上, foreach 将略微快于 for . 因为数组上的 foreach 基本上编译为:

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

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

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

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

  • 1

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

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

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

相关问题