C#中控制结构“for”和“foreach”的性能差异

105
哪个代码片段能提供更好的性能?以下代码片段使用C#编写。 1.
for(int tempCount=0;tempCount<list.count;tempcount++)
{
    if(list[tempCount].value==value)
    {
        // Some code.
    }
}
foreach(object row in list)
{
    if(row.value==value)
    {
        //Some coding
    }
}

31
我想这并不是很重要。如果你正遇到性能问题,几乎肯定不是由于此引起的。并不是说你不应该问这个问题... - darasd
2
除非你的应用程序非常注重性能,否则我不会担心这个问题。更好的做法是编写清晰易懂的代码。 - Fortyrunner
2
让我担心的是,这里的一些答案似乎是由那些根本没有迭代器概念的人发布的,因此也没有枚举器或指针的概念。 - Ed James
3
第二段代码无法编译。System.Object没有名为'value'的成员(除非你真的很糟糕,已将其定义为扩展方法并且正在比较委托)。请为你的foreach指定强类型。 - Trillian
1
第一段代码也不会编译通过,除非 list 的类型确实有一个 count 成员而不是 Count - Jon Skeet
显示剩余2条评论
10个回答

130

这部分取决于list的确切类型,也取决于您使用的CLR。

无论循环中是否进行了任何实际工作,它是否有任何显著意义将取决于情况。在几乎所有情况下,性能差异并不显著,但可读性方面foreach循环更为有利。

我个人会使用LINQ来避免使用“if”:

foreach (var item in list.Where(condition))
{
}

编辑:对那些声称使用 foreach 迭代 List<T> 会产生与 for 循环相同代码的人,这里有证据表明它们并不相同:

static void IterateOverList(List<object> list)
{
    foreach (object o in list)
    {
        Console.WriteLine(o);
    }
}

生成以下语言的中间代码:

.method private hidebysig static void  IterateOverList(class [mscorlib]System.Collections.Generic.List`1<object> list) cil managed
{
  // Code size       49 (0x31)
  .maxstack  1
  .locals init (object V_0,
           valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator<object> V_1)
  IL_0000:  ldarg.0
  IL_0001:  callvirt   instance valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator<!0> class [mscorlib]System.Collections.Generic.List`1<object>::GetEnumerator()
  IL_0006:  stloc.1
  .try
  {
    IL_0007:  br.s       IL_0017
    IL_0009:  ldloca.s   V_1
    IL_000b:  call       instance !0 valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator<object>::get_Current()
    IL_0010:  stloc.0
    IL_0011:  ldloc.0
    IL_0012:  call       void [mscorlib]System.Console::WriteLine(object)
    IL_0017:  ldloca.s   V_1
    IL_0019:  call       instance bool valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator<object>::MoveNext()
    IL_001e:  brtrue.s   IL_0009
    IL_0020:  leave.s    IL_0030
  }  // end .try
  finally
  {
    IL_0022:  ldloca.s   V_1
    IL_0024:  constrained. valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator<object>
    IL_002a:  callvirt   instance void [mscorlib]System.IDisposable::Dispose()
    IL_002f:  endfinally
  }  // end handler
  IL_0030:  ret
} // end of method Test::IterateOverList

编译器对数组的处理方式不同,将foreach循环基本上转换为for循环,但不包括List<T>。以下是数组的等效代码:

static void IterateOverArray(object[] array)
{
    foreach (object o in array)
    {
        Console.WriteLine(o);
    }
}

// Compiles into...

.method private hidebysig static void  IterateOverArray(object[] 'array') cil managed
{
  // Code size       27 (0x1b)
  .maxstack  2
  .locals init (object V_0,
           object[] V_1,
           int32 V_2)
  IL_0000:  ldarg.0
  IL_0001:  stloc.1
  IL_0002:  ldc.i4.0
  IL_0003:  stloc.2
  IL_0004:  br.s       IL_0014
  IL_0006:  ldloc.1
  IL_0007:  ldloc.2
  IL_0008:  ldelem.ref
  IL_0009:  stloc.0
  IL_000a:  ldloc.0
  IL_000b:  call       void [mscorlib]System.Console::WriteLine(object)
  IL_0010:  ldloc.2
  IL_0011:  ldc.i4.1
  IL_0012:  add
  IL_0013:  stloc.2
  IL_0014:  ldloc.2
  IL_0015:  ldloc.1
  IL_0016:  ldlen
  IL_0017:  conv.i4
  IL_0018:  blt.s      IL_0006
  IL_001a:  ret
} // end of method Test::IterateOverArray

有趣的是,我在 C# 3 规范中找不到这个记录...


Jon,顺便问一下,上面的List<T>场景...是否也适用于其他集合?另外,您是如何知道这个问题的答案(完全没有恶意)...是因为之前偶然发现的吗?这太...随机/神秘了 :) - Pure.Krome
5
我已经意识到数组优化有一段时间了 - 数组是一种“核心”集合类型;C#编译器已经深刻地意识到它们,因此对它们进行不同的处理是有道理的。编译器不应该也没有任何特殊知识关于List<T> - Jon Skeet
3
优化列表迭代器可能会改变在迭代期间修改列表时的行为。你将失去抛出“如果已修改则异常”的功能。仍然可以进行优化,但需要检查是否发生了修改(包括其他线程上的修改,我认为)。 - Craig Gidney
@Strilanc:是的,它会改变行为。这也可能是它没有被优化掉的原因之一 :) - Jon Skeet
5
微软在2004年曾这样说过。a) 事情会发生变化;b) 为了使这一点显著,工作必须在每次迭代中执行微小的量级。请注意,对数组进行foreach循环与使用for相当。始终首先考虑可读性,只有在有证据表明它能带来可衡量的性能优势时才进行微观优化。 - Jon Skeet
显示剩余5条评论

14

for循环被编译成大致等价于以下代码:

int tempCount = 0;
while (tempCount < list.Count)
{
    if (list[tempCount].value == value)
    {
        // Do something
    }
    tempCount++;
}

相比之下,foreach循环被编译成大致等同于以下代码的代码:

using (IEnumerator<T> e = list.GetEnumerator())
{
    while (e.MoveNext())
    {
        T o = (MyClass)e.Current;
        if (row.value == value)
        {
            // Do something
        }
    }
}

所以你可以看到,这将完全取决于枚举器的实现方式与列表索引器的实现方式。事实证明,基于数组的类型的枚举器通常是这样编写的:

private static IEnumerable<T> MyEnum(List<T> list)
{
    for (int i = 0; i < list.Count; i++)
    {
        yield return list[i];
    }
}

所以你可以看到,在这种情况下,它不会有太大的影响,但是链表的枚举器可能看起来像这样:

private static IEnumerable<T> MyEnum(LinkedList<T> list)
{
    LinkedListNode<T> current = list.First;
    do
    {
        yield return current.Value;
        current = current.Next;
    }
    while (current != null);
}

.NET中,您会发现LinkedList<T>类甚至没有索引器,因此您将无法在链表上执行for循环;但是如果您可以的话,索引器应该编写如下:
public T this[int index]
{
       LinkedListNode<T> current = this.First;
       for (int i = 1; i <= index; i++)
       {
            current = current.Next;
       }
       return current.value;
}

正如您所看到的,循环中多次调用此函数将比使用可以记住列表中位置的枚举器慢得多。


在for循环中多次调用此函数会导致性能不佳,但是糟糕的索引函数并不是反对使用for的理由,只是反对使用糟糕设计的函数。 for循环不需要索引器,可以完美地使用LinkedListNode<T>而不是int,从而消除了对这个“搜索索引循环”的需求。 很可能C#开发人员没有为LinkedList包含索引器,以防止人们直接从List和数组中移植代码,而没有意识到它将是O(N)查找,而不是其他类型的O(1)。 - Paul Childs

12

一个简单的半验证测试。我进行了一个小测试,只是为了看看。以下是代码:

static void Main(string[] args)
{
    List<int> intList = new List<int>();

    for (int i = 0; i < 10000000; i++)
    {
        intList.Add(i);
    }

    DateTime timeStarted = DateTime.Now;
    for (int i = 0; i < intList.Count; i++)
    {
        int foo = intList[i] * 2;
        if (foo % 2 == 0)
        {
        }
    }

    TimeSpan finished = DateTime.Now - timeStarted;

    Console.WriteLine(finished.TotalMilliseconds.ToString());
    Console.Read();

}

以下是 foreach 部分:

foreach (int i in intList)
{
    int foo = i * 2;
    if (foo % 2 == 0)
    {
    }
}

当我将for替换为foreach时,foreach始终比for快20毫秒。for的时间是135-139ms,而foreach的时间是113-119ms。我来回多次进行了交换,确保它不是某个过程刚开始才产生的结果。
然而,当我删除foo和if语句时,for比foreach快30毫秒(foreach为88ms,for为59ms)。它们都是空壳。我假设foreach实际上传递了一个变量,而for只是在增加一个变量。如果我添加
int foo = intList[i];

然后使用for循环时速度变慢了约30ms。我认为这可能与它创建foo并获取数组中的变量并将其分配给foo有关。如果只访问intList[i],则不会有这样的惩罚。
老实说...我希望在所有情况下foreach都会稍微慢一点,但大多数应用程序中并没有太大影响。
编辑:这是使用Jon的建议编写的新代码(在System.OutOfMemory异常抛出之前,最大的int为134217728):
static void Main(string[] args)
{
    List<int> intList = new List<int>();

    Console.WriteLine("Generating data.");
    for (int i = 0; i < 134217728 ; i++)
    {
        intList.Add(i);
    }

    Console.Write("Calculating for loop:\t\t");

    Stopwatch time = new Stopwatch();
    time.Start();
    for (int i = 0; i < intList.Count; i++)
    {
        int foo = intList[i] * 2;
        if (foo % 2 == 0)
        {
        }
    }

    time.Stop();
    Console.WriteLine(time.ElapsedMilliseconds.ToString() + "ms");
    Console.Write("Calculating foreach loop:\t");
    time.Reset();
    time.Start();

    foreach (int i in intList)
    {
        int foo = i * 2;
        if (foo % 2 == 0)
        {
        }
    }

    time.Stop();

    Console.WriteLine(time.ElapsedMilliseconds.ToString() + "ms");
    Console.Read();
}

以下是结果:

正在生成数据。 计算for循环:2458毫秒 计算foreach循环:2005毫秒

将它们交换以查看它是否处理事物的顺序会产生几乎相同的结果。


6
使用计时器(Stopwatch)比使用DateTime.Now更好,说实话我不会信任那么快的运行。 - Jon Skeet
9
你的foreach循环运行更快,因为'for'循环在每次迭代时都会评估条件。在你的例子中,这会导致多出一个方法调用(获取list.count)。简而言之,你正在对两个不同的代码片段进行基准测试,因此结果看起来很奇怪。尝试使用'int max = intlist.Count; for(int i = 0; i<max; i++)...','for'循环将始终比foreach循环运行得更快,这是预期的! - A.R.
1
编译后,对于原始类型,for和foreach进行优化时完全相同。只有当引入List<T>时,它们在速度上才会有很大的差异。 - DotNetRussell

9

注意:这个答案更适用于Java而非C#,因为C#在LinkedLists上没有索引器,但我认为总体观点仍然成立。

如果你正在使用的list是一个LinkedList,那么对于大型列表,使用下标代码(类似数组的访问)的性能比使用foreach中的IEnumerator要差得多。

当您使用下标语法:list[10000]访问LinkedList中的第10000个元素时,链表将从头结点开始,并遍历Next指针10000次,直到到达正确的对象。显然,如果您在循环中这样做,你会得到:

list[0]; // head
list[1]; // head.Next
list[2]; // head.Next.Next
// etc.

当您调用GetEnumerator(隐式使用forach语法)时,您将获得一个指向头节点的IEnumerator对象。每次调用MoveNext时,该指针将移动到下一个节点,如下所示:
IEnumerator em = list.GetEnumerator();  // Current points at head
em.MoveNext(); // Update Current to .Next
em.MoveNext(); // Update Current to .Next
em.MoveNext(); // Update Current to .Next
// etc.

正如您所看到的,在LinkedList的情况下,数组索引器方法会变得越来越慢,循环次数越多(它必须一遍又一遍地通过相同的头指针)。而IEnumerable只在常数时间内运行。
当然,正如Jon所说,这实际上取决于list的类型,如果list不是LinkedList而是一个数组,则行为完全不同。

4
在.NET中,LinkedList没有索引器,因此它实际上不是一种选择。 - Jon Skeet
哦,那么问题就解决了 :-) 我正在查看MSDN上的LinkedList<T>文档,它有一个相当不错的API。最重要的是,它没有像Java一样的get(int index)方法。尽管如此,我想这个观点仍然适用于任何其他类似列表的数据结构,只要它们公开了比特定的IEnumerator更慢的索引器。 - Tom Lokhorst

2

正如其他人提到的一样,性能实际上并不重要,因为循环中使用了 IEnumerable/IEnumerator,所以 foreach 总是会慢一点。编译器将该结构转换为对该接口的调用,并且在 foreach 结构的每一步中都会调用一个函数和一个属性。

IEnumerator iterator = ((IEnumerable)list).GetEnumerator();
while (iterator.MoveNext()) {
  var item = iterator.Current;
  // do stuff
}

这是C#中结构的等效扩展。您可以想象基于MoveNext和Current的实现如何影响性能。而在数组访问中,您没有这些依赖关系。


4
不要忘记数组访问和索引器访问之间的区别。如果list是一个List<T>,那么调用索引器仍然会导致(可能是内联的)开销。这不像是裸的数组访问。 - Jon Skeet
非常正确!这又是一个属性执行,我们要看实现的情况。 - Charles Prakash Dasari

2
阅读了足够多的“应该优先选择 foreach 循环以提高可读性”的论点之后,我可以说我的第一反应是“什么”?可读性是主观的,而在这个特定的实例中更加如此。对于具有编程背景(几乎所有 Java 之前的语言)的人来说,for 循环比 foreach 循环更容易阅读。此外,声称 foreach 循环更易读的同样的人也支持 linq 和其他“特性”,这些特性使代码难以阅读和维护,这证明了上述观点。
关于性能影响,请参见 this 问题的答案。
编辑:C# 中有一些集合(例如 HashSet)没有索引器。在这些集合中,foreach 是唯一的迭代方式,这是我认为它应该用于 for 之上的唯一情况。

0
在您提供的示例中,使用foreach循环而不是for循环肯定更好。
标准的foreach结构比简单的for-loop(每步2个周期)更快(每步1.5个周期),除非循环已展开(每步1.0个周期)。
因此,在日常代码中,性能不是使用更复杂的forwhiledo-while结构的原因。
请查看此链接:http://www.codeproject.com/Articles/146797/Fast-and-Less-Fast-Loops-in-C
╔══════════════════════╦═══════════╦═══════╦════════════════════════╦═════════════════════╗
║        Method        ║ List<int> ║ int[] ║ Ilist<int> onList<Int> ║ Ilist<int> on int[] ║
╠══════════════════════╬═══════════╬═══════╬════════════════════════╬═════════════════════╣
║ Time (ms)            ║ 23,80     ║ 17,56 ║ 92,33                  ║ 86,90               ║
║ Transfer rate (GB/s) ║ 2,82      ║ 3,82  ║ 0,73                   ║ 0,77                ║
║ % Max                ║ 25,2%     ║ 34,1% ║ 6,5%                   ║ 6,9%                ║
║ Cycles / read        ║ 3,97      ║ 2,93  ║ 15,41                  ║ 14,50               ║
║ Reads / iteration    ║ 16        ║ 16    ║ 16                     ║ 16                  ║
║ Cycles / iteration   ║ 63,5      ║ 46,9  ║ 246,5                  ║ 232,0               ║
╚══════════════════════╩═══════════╩═══════╩════════════════════════╩═════════════════════╝


4
你可以重新阅读你所链接的代码项目文章。这是一篇有趣的文章,但它与你的帖子完全相反。此外,你重新创建的表格衡量访问数组和列表及其IList接口的性能,与问题无关。 :) - Paul Walls

0

你可以在深入 .NET - 第一部分 迭代中了解更多相关信息。

它覆盖了从.NET源代码到反汇编结果的所有内容(不包括第一个初始化)。

例如-使用foreach循环进行数组迭代: 输入图像描述

和-使用foreach循环进行列表迭代: 输入图像描述

最终结果如下: 输入图像描述

enter image description here


0

在测试两个循环的速度时,有一个更有趣的事实很容易被忽略:使用调试模式不会让编译器使用默认设置来优化代码。

这导致了一个有趣的结果,即在调试模式下,foreach 比 for 循环更快。而在发布模式下,for 循环比 foreach 更快。显然,编译器有更好的方法来优化 for 循环,而 foreach 循环则涉及多个方法调用,因此更难优化。顺便说一句,for 循环是如此基础,以至于甚至可能被 CPU 自身优化。


0
在C#中,for循环foreach循环更快,因为for循环利用索引来迭代元素,而foreach循环利用生成新对象的enumerator来迭代元素。
为了验证这个说法,让我们看一个示例程序:
using System;
using System.Diagnostics;

class MainProgram
{
    static void Main()
    {
        int[] randomNumbers = new int[9999999];

        var randomGenerate = new Random();
        
        // Random values are initialized to array
        for (int i = 0; i < randomNumbers.Length; i++)
        {
            randomNumbers[i] = randomGenerate.Next(100);
        }

        Stopwatch timer = new Stopwatch();

        // Calculate the time taken by the for loop
        timer.Start();
        for (int i = 0; i < randomNumbers.Length; i++)
        {
            int x = randomNumbers[i];
        }
        timer.Stop();
        Console.WriteLine("Time taken by For loop: " + timer.ElapsedMilliseconds + "ms");

        // Calculate the time taken by the foreach loop
        timer.Reset();
        timer.Start();
        foreach (int x in randomNumbers)
        {
            int y = x;
        }
        timer.Stop();
        Console.WriteLine("Time taken by Foreach loop: " + timer.ElapsedMilliseconds + "ms");
    }
}

这个程序的输出因系统性能而异,但每次执行时,for循环都比foreach循环快。

Output:
    Time taken by For loop: 20ms
    Time taken by Foreach loop: 25ms


在某些情况下,foreach循环比for循环更快,但性能的差异将是可以忽略不计的。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接