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

时间:2009-07-14 11:16:33

标签: c# performance for-loop foreach

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

1

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

2

foreach(MyType current in list)
{
    current.DoSomething();
}

9 个答案:

答案 0 :(得分:130)

嗯,这在一定程度上取决于list的确切类型。它还取决于你正在使用的确切CLR。

它是否以任何方式重要取决于你是否在循环中做任何实际工作。在几乎所有的情况下,性能的差异不会很大,但可读性的差异有利于foreach循环。

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

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

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

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

产生IL:

.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规范中记录的内容......

答案 1 :(得分: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&lt; T&gt; class甚至没有索引器,因此你无法在链表上进行for循环;但如果可以的话,索引器就必须这样编写:

public T this[int index]
{
       LinkedListNode<T> current = this.First;
       for (int i = 1; i <= index; i++)
       {
            current = current.Next;
       }
       return current.value;
}

正如您所看到的,在循环中多次调用它会比使用能够记住它在列表中的位置的枚举器慢得多。

答案 2 :(得分: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)
    {
    }
}

当我用foreach替换for时 - foreach的速度提高了20毫秒 - 始终。 for为135-139ms,而foreach为113-119ms。我来回交换几次,确保这不是一个刚开始的过程。

然而,当我删除了foo和if语句时,for的速度提高了30 ms(foreach为88ms,为59ms)。他们都是空壳。我假设foreach实际上传递了一个变量,因为for只是递增一个变量。如果我添加了

int foo = intList[i];

然后for变得慢约30ms。我假设这与创建foo并抓取数组中的变量并将其分配给foo有关。如果您只是访问intList [i],那么您没有那个惩罚。

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

编辑:这是使用Jons建议的新代码(134217728是在抛出System.OutOfMemory异常之前可以拥有的最大的int):

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

交换它们以查看它是否处理事物的顺序会产生相同的结果(差不多)。

答案 3 :(得分:9)

注意:这个答案更适用于Java而不是C#,因为C#在LinkedLists上没有索引器,但我认为一般的观点仍然存在。

如果您正在使用的list恰好是LinkedList,则索引器代码(数组样式访问)的性能要比使用IEnumerator中的foreach表示大型列表。

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

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 s的情况下,数组索引器方法变得越来越慢,循环越长(它必须一遍又一遍地通过相同的头指针)。而IEnumerable只是在恒定的时间内运作。

当然,正如Jon所说,这实际上取决于list的类型,如果list不是LinkedList,而是数组,则行为完全不同。

答案 4 :(得分:2)

与其他人提到的一样,虽然性能实际上并不重要,但由于循环中的IEnumerable / IEnumerator用法,foreach总是会慢一点。编译器将构造转换为该接口上的调用,并且对于每个步骤,在foreach构造中调用函数+属性。

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

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

答案 5 :(得分:1)

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

关于对效果的影响,请参阅this问题的答案。

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

答案 6 :(得分:0)

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

这让我得到了一个有趣的结果,即foreach比调试模式更快。而在发布模式中,fort比foreach更快。显然,编译器有更好的方法来优化for循环而不是foreach循环,这会影响多个方法调用。 for循环就是这样的基础,它甚至可以由CPU本身进行优化。

答案 7 :(得分: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               ║
╚══════════════════════╩═══════════╩═══════╩════════════════════════╩═════════════════════╝

答案 8 :(得分:0)

您可以在Deep .NET - part 1 Iteration

中了解它

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

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

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

和最终结果: enter image description here

enter image description here