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

发布于 2024-07-25 05:56:44 字数 1159 浏览 6 评论 0原文

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

I was trying to figure out if a for loop was faster than a foreach loop and was using the System.Diagnostics classes to time the task. While running the test I noticed that which ever loop I put first always executes slower then the last one. Can someone please tell me why this is happening? My code is below:

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

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(8

归属感 2024-08-01 05:56:45

原因是 foreach 版本中有几种形式的开销,而这些开销在 for 循环

  • 使用 IDisposable 中是不存在的。
  • 每个元素都有一个额外的方法调用。 每个元素都必须使用 IEnumerator.Current 进行底层访问,这是一个方法调用。 因为它位于接口上,所以无法内联。 这意味着 N 个方法调用,其中 N 是枚举中的元素数量。 for 循环仅使用索引器
  • 在 foreach 循环中,所有调用都通过一个接口。 一般来说,这比通过具体类型要慢一些。

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

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

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

  • 您应该使用秒表而不是日期时间。 对于性能基准来说,它要准确得多。
  • 您应该多次执行测试,而不仅仅是一次。
  • 您需要在每个循环上进行一次虚拟运行,以消除第一次 JIT 方法带来的问题。 当所有代码都在同一个方法中时,这可能不是问题,但也没有什么坏处。
  • 您需要使用列表中的 4 个以上值。 尝试 40,000。

The reason why is there are several forms of overhead in the foreach version that are not present in the for loop

  • Use of an IDisposable.
  • An additional method call for every element. Each element must be accessed under the hood by using IEnumerator<T>.Current which is a method call. Because it's on an interface it cannot be inlined. This means N method calls where N is the number of elements in the enumeration. The for loop just uses and indexer
  • In a foreach loop all calls go through an interface. In general this a bit slower than through a concrete type

Please note that the things I listed above are not necessarily huge costs. They are typically very small costs that can contribute to a small performance difference.

Also note, as Mehrdad pointed out, the compilers and JIT may choose to optimize a foreach loop for certain known data structures such as an array. The end result may just be a for loop.

Note: Your performance benchmark in general needs a bit more work to be accurate.

  • You should use a StopWatch instead of DateTime. It is much more accurate for performance benchmarks.
  • You should perform the test many times not just once
  • You need to do a dummy run on each loop to eliminate the problems that come with JITing a method the first time. This probably isn't an issue when all of the code is in the same method but it doesn't hurt.
  • You need to use more than just 4 values in the list. Try 40,000 instead.
治碍 2024-08-01 05:56:45

您应该使用秒表来计时该行为。

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

You should be using the StopWatch to time the behavior.

Technically the for loop is faster. Foreach calls the MoveNext() method (creating a method stack and other overhead from a call) on the IEnumerable's iterator, when for only has to increment a variable.

谁的年少不轻狂 2024-08-01 05:56:45

我不明白为什么这里的每个人都说在这种特殊情况下 for 会比 foreach 更快。 对于 List,通过 List foreach 比通过 Listfor 慢大约 2 倍;)。

事实上,foreach 会比这里的 for 稍微。 因为数组上的 foreach 本质上会编译为:

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

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

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

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

I don't see why everyone here says that for would be faster than foreach in this particular case. For a List<T>, it is (about 2x slower to foreach through a List than to for through a List<T>).

In fact, the foreach will be slightly faster than the for here. Because foreach on an array essentially compiles to:

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

Using .Length as a stop criteria allows the JIT to remove bounds checks on the array access, since it's a special case. Using i < 4 makes the JIT insert extra instructions to check each iteration whether or not i is out of bounds of the array, and throw an exception if that is the case. However, with .Length, it can guarantee you'll never go outside of the array bounds so the bounds checks are redundant, making it faster.

However, in most loops, the overhead of the loop is insignificant compared to the work done inside.

The discrepancy you're seeing can only be explained by the JIT I guess.

聽兲甴掵 2024-08-01 05:56:45

我不会对此进行太多解读 - 由于以下原因,这不是一个好的分析代码
1. DateTime 不适用于分析。 您应该使用 QueryPerformanceCounter 或 StopWatch,它们使用 CPU 硬件配置文件计数器
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毫秒

所以基本上在我看来,无论你第二次运行什么代码,运行速度都会稍微快一点,但不会
足以具有任何意义。

--编辑--
这里有一些有用的链接
如何使用 QueryPerformanceCounter 对托管代码进行计时
指令缓存
乱序执行

I wouldn't read too much into this - this isn't good profiling code for the following reasons
1. DateTime isn't meant for profiling. You should use QueryPerformanceCounter or StopWatch which use the CPU hardware profile counters
2. Console.WriteLine is a device method so there may be subtle effects such as buffering to take into account
3. Running one iteration of each code block will never give you accurate results because your CPU does a lot of funky on the fly optimisation such as out of order execution and instruction scheduling
4. Chances are the code that gets JITed for both code blocks is fairly similar so is likely to be in the instruction cache for the second code block

To get a better idea of timing, I did the following

  1. Replaced the Console.WriteLine with a math expression ( e^num)
  2. I used QueryPerformanceCounter/QueryPerformanceTimer through P/Invoke
  3. I ran each code block 1 million times then averaged the results

When I did that I got the following results:

The for loop took 0.000676 milliseconds
The foreach loop took 0.000653 milliseconds

So foreach was very slightly faster but not by much

I then did some further experiments and ran the foreach block first and the for block second
When I did that I got the following results:

The foreach loop took 0.000702 milliseconds
The for loop took 0.000691 milliseconds

Finally I ran both loops together twice i.e for + foreach then for + foreach again
When I did that I got the following results:

The foreach loop took 0.00140 milliseconds
The for loop took 0.001385 milliseconds

So basically it looks to me that whatever code you run second, runs very slightly faster but not
enough to be of any significance.

--Edit--
Here are a couple of useful links
How to time managed code using QueryPerformanceCounter
The instruction cache
Out of order execution

一袭水袖舞倾城 2024-08-01 05:56:44

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

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

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

Probably because the classes (e.g. Console) need to be JIT-compiled the first time through. You'll get the best metrics by calling all methods (to JIT them (warm then up)) first, then performing the test.

As other users have indicated, 4 passes is never going to be enough to to show you the difference.

Incidentally, the difference in performance between for and foreach will be negligible and the readability benefits of using foreach almost always outweigh any marginal performance benefit.

小红帽 2024-08-01 05:56:44
  1. 我不会使用 DateTime 来衡量性能 - 尝试使用 Stopwatch 类。
  2. 仅通过 4 次进行测量永远不会给您带来好的结果。 更好的使用> 100.000 次通过(您可以使用外循环)。 不要在循环中执行 Console.WriteLine
  3. 更好的是:使用分析器(例如 Redgate ANTS 或 NProf)
  1. I would not use DateTime to measure performance - try the Stopwatch class.
  2. Measuring with only 4 passes is never going to give you a good result. Better use > 100.000 passes (you can use an outer loop). Don't do Console.WriteLine in your loop.
  3. Even better: use a profiler (like Redgate ANTS or maybe NProf)
世界等同你 2024-08-01 05:56:44

我对 C# 不太了解,但当我没记错时,微软正在为 Java 构建“Just in Time”编译器。 当他们在 C# 中使用相同或相似的技术时,“一些其次的结构执行得更快”是很自然的。

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

Addon:另一个评论是:输出模块可以在第一个循环中第一次进行 JITed,这对我来说比我的第一个猜测更有可能。 现代语言非常复杂,要找出其幕后所做的事情是非常复杂的。 我的这个陈述也符合这个猜测:

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

I am not so much in C#, but when I remember right, Microsoft was building "Just in Time" compilers for Java. When they use the same or similar techniques in C#, it would be rather natural that "some constructs coming second perform faster".

For example it could be, that the JIT-System sees that a loop is executed and decides adhoc to compile the whole method. Hence when the second loop is reached, it is yet compiled and performs much faster than the first. But this is a rather simplistic guess of mine. Of course you need a far greater insight in the C# runtime system to understand what is going on. It could also be, that the RAM-Page is accessed first in the first loop and in the second it is still in the CPU-cache.

Addon: The other comment that was made: that the output module can be JITed a first time in the first loop seams to me more likely than my first guess. Modern languages are just very complex to find out what is done under the hood. Also this statement of mine fits into this guess:

But also you have terminal-outputs in your loops. They make things yet more difficult. It could also be, that it costs some time to open the terminal a first time in a program.

柠檬 2024-08-01 05:56:44

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

I was just performing tests to get some real numbers, but in the meantime Gaz beat me to the answer - the call to Console.Writeline is jitted at the first call, so you pay that cost in the first loop.

Just for information though - using a stopwatch rather than the datetime and measuring number of ticks:

Without a call to Console.Writeline before the first loop the times were

for: 16802
foreach: 2282

with a call to Console.Writeline they were

for: 2729
foreach: 2268

Though these results were not consistently repeatable because of the limited number of runs, but the magnitude of difference was always roughly the same.


The edited code for reference:

        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();
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文