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

Performance difference for control structures 'for' and 'foreach' in C#

哪个代码段可以提供更好的性能?以下代码段是用C编写的。

1。

1
2
3
4
for(int counter=0; counter<list.Count; counter++)
{
    list[counter].DoSomething();
}

2。

1
2
3
4
foreach(MyType current in list)
{
    current.DoSomething();
}


嗯,部分取决于list的确切类型。它还将取决于您使用的确切的CLR。

它是否有意义取决于你是否在循环中做了任何真正的工作。在几乎所有的情况下,性能的差异都不显著,但是可读性的差异有利于foreach循环。

我个人也会使用LINQ来避免"if"的出现:

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

编辑:对于那些声称用foreach迭代List会产生与for循环相同的代码的人,这里有证据表明它没有:

1
2
3
4
5
6
7
static void IterateOverList(List<object> list)
{
    foreach (object o in list)
    {
        Console.WriteLine(o);
    }
}

生产IL:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
.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循环。以下是数组的等效代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
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规范中记录的这个……


一个for循环被编译成与此大致相等的代码:

1
2
3
4
5
6
7
8
9
int tempCount = 0;
while (tempCount < list.Count)
{
    if (list[tempCount].value == value)
    {
        // Do something
    }
    tempCount++;
}

其中,作为一个foreach循环,编译成与此大致相等的代码:

1
2
3
4
5
6
7
8
9
10
11
using (IEnumerator<T> e = list.GetEnumerator())
{
    while (e.MoveNext())
    {
        T o = (MyClass)e.Current;
        if (row.value == value)
        {
            // Do something
        }
    }
}

如您所见,这完全取决于枚举器是如何实现的,而列表索引器是如何实现的。事实证明,基于数组的类型的枚举器通常编写如下:

1
2
3
4
5
6
7
private static IEnumerable<T> MyEnum(List<T> list)
{
    for (int i = 0; i < list.Count; i++)
    {
        yield return list[i];
    }
}

因此,正如您所看到的,在这种情况下,它不会有太大的区别,但是链表的枚举器可能会如下所示:

1
2
3
4
5
6
7
8
9
10
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类甚至没有索引器,因此您将无法在链接列表上执行for循环;但如果可以,则必须这样编写索引器:

1
2
3
4
5
6
7
8
9
public T this[int index]
{
       LinkedListNode<T> current = this.First;
       for (int i = 1; i <= index; i++)
       {
            current = current.Next;
       }
       return current.value;
}

如您所见,在一个循环中多次调用这个函数要比使用一个能记住它在列表中的位置的枚举器慢得多。


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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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部分:

1
2
3
4
5
6
7
foreach (int i in intList)
{
    int foo = i * 2;
    if (foo % 2 == 0)
    {
    }
}

当我将for替换为foreach时——foreach比原来快了20毫秒——始终如一。结果是135-139ms,前臂113-119ms,我来回换了几次,确保不是某个过程刚刚开始。

但是,当我删除foo和if语句时,for的速度快了30毫秒(foreach为88毫秒,for为59毫秒)。他们都是空壳。我假设foreach实际上传递了一个变量,其中as-for只是递增一个变量。如果我增加

1
int foo = intList[i];

然后for会变慢大约30毫秒。我假设这与它创建foo和获取数组中的变量并将其分配给foo有关。如果你只需要进入名单,那么你就不会受到惩罚。

说实话……我希望foreach在所有情况下都会稍微慢一点,但在大多数应用程序中都不重要。

编辑:下面是使用jons建议的新代码(134217728是抛出system.outofmemory异常之前可以得到的最大整数):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
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();
}

结果如下:

正在生成数据。回路计算:2458ms计算foreach循环:2005ms

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


注意:对于C语言,这个答案比Java更适用于Java语言,因为C语言在EDOCX1 0中没有索引器,但我认为一般点仍然成立。

如果您所使用的list碰巧是LinkedList,那么索引器代码(数组样式访问)的性能要比使用foreach中的IEnumerator用于大列表差得多。

当使用索引器语法:list[10000]访问LinkedList中的元素10.000时,链接列表将从head节点开始,并遍历Next指针一万次,直到到达正确的对象。显然,如果你在一个循环中这样做,你会得到:

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

当您调用GetEnumerator时(隐式使用forach语法),您将得到一个IEnumerator对象,该对象具有指向head节点的指针。每次调用MoveNext时,该指针都会移动到下一个节点,如下所示:

1
2
3
4
5
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只是在固定时间内运行。

当然,正如乔恩所说,这确实取决于list的类型,如果list不是LinkedList,而是一个数组,则行为完全不同。


像其他人提到的那样,尽管性能实际上并不重要,但是由于循环中使用了IEnumerableIEnumerator,foreach总是会慢一点。编译器将构造转换为该接口上的调用,并在foreach构造中为每个步骤调用一个函数+一个属性。

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

这是C中构造的等效展开。您可以想象性能影响如何根据moveNext和current的实现而变化。而在数组访问中,您没有这种依赖关系。


在阅读了足够多的"foreach循环应该是可读性的首选"论点之后,我可以说我的第一个反应是"什么"?可读性,一般来说,是主观的,在这个特定的例子中,甚至更多。对于有编程背景的人(实际上,Java之前的每种语言),for循环比Frach循环要容易得多。此外,声称foreach循环更具可读性的人,也是linq和其他"特性"的支持者,这些特性使代码难以读取和维护,这证明了上述观点。

关于对性能的影响,请参阅此问题的答案。

编辑:C(如哈希集)中有没有索引器的集合。在这些集合中,foreach是唯一迭代的方法,我认为它是唯一应该使用它的情况。


您可以在deep.net中阅读它-第1部分迭代

它涵盖了从.NET源代码一直到反汇编的结果(没有第一次初始化)。

例如-使用foreach循环的数组迭代:enter image description here

和-列出foreach循环的迭代:enter image description here

最终结果是:enter image description here

enter image description here


在您提供的示例中,最好使用foreach循环,而不是for循环。

标准的foreach构造可以比简单的for-loop构造更快(每步骤1,5个循环),除非循环已经展开(每步骤1.0个循环)。

因此,对于日常代码,性能不是使用更复杂的forwhiledo-while构造的原因。

查看此链接:http://www.codeproject.com/articles/146797/fast-and-less-fast-loops-in-c

1
2
3
4
5
6
7
8
9
10
╔══════════════════════╦═══════════╦═══════╦════════════════════════╦═════════════════════╗
║        Method        ║ List<int>int[] ║ Ilist<int> onList<Int> ║ Ilist<int> on&nbsp;int[]
╠══════════════════════╬═══════════╬═══════╬════════════════════════╬═════════════════════╣
║ Time (ms)            ║ 23,80     ║ 17,5692,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               ║
╚══════════════════════╩═══════════╩═══════╩════════════════════════╩═════════════════════╝


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

这使我得到一个有趣的结果,在调试模式下foreach比for更快。而在释放模式下,for ist比foreach更快。显然,编译器有更好的方法来优化for循环,而不是foreach循环,后者会破坏几个方法调用。for循环是如此的基本,以至于它甚至可能由CPU本身优化。