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

Kth*_*var 104 c# performance foreach for-loop

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

1.

for(int counter=0; counter<list.Count; counter++)
{
    list[counter].DoSomething();
}
Run Code Online (Sandbox Code Playgroud)

2.

foreach(MyType current in list)
{
    current.DoSomething();
}
Run Code Online (Sandbox Code Playgroud)

Jon*_*eet 130

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

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

我个人使用LINQ来避免"if":

foreach (var item in list.Where(condition))
{
}
Run Code Online (Sandbox Code Playgroud)

编辑:对于那些你谁是声称迭代一个List<T>foreach产生相同的代码for回路,这里的证据表明,它并不:

static void IterateOverList(List<object> list)
{
    foreach (object o in list)
    {
        Console.WriteLine(o);
    }
}
Run Code Online (Sandbox Code Playgroud)

产生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
Run Code Online (Sandbox Code Playgroud)

编译器以不同方式处理数组,将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
Run Code Online (Sandbox Code Playgroud)

有趣的是,我无法在任何地方找到C#3规范中记录的内容......

  • 我已经了解了一段时间的数组优化 - 数组是一种"核心"类型的集合; C#编译器已经非常了解它们,因此以不同方式对待它们是有意义的.编译器不(也不应该)具有`List <T>`的任何特殊知识. (5认同)
  • @VeeKeyBee:2004年微软说过.a)事情发生了变化; b)这项工作必须在每次迭代时进行*微小*的工作才能实现这一点.注意,数组上的`foreach`相当于`for`.*始终*代码首先是可读性,然后只有微量优化才有*证据*它可以带来可衡量的性能优势. (5认同)
  • @JonSkeet优化列表迭代器会在迭代期间修改列表时更改行为.如果修改了,您将失去异常.它仍然可以优化,但需要检查没有发生任何修改(包括在其他线程上,我假设). (3认同)

Mar*_*own 14

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

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

凡为foreach循环被编译的代码大约相当于这个:

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

正如您所看到的,这将取决于枚举器的实现方式与列表索引器的实现方式.事实证明,基于数组的类型的枚举器通常写成如下:

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

正如您所看到的,在这种情况下它不会产生太大的区别,但是链表的枚举器可能看起来像这样:

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

.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;
}
Run Code Online (Sandbox Code Playgroud)

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


Ken*_*ann 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();

}
Run Code Online (Sandbox Code Playgroud)

以下是foreach部分:

foreach (int i in intList)
{
    int foo = i * 2;
    if (foo % 2 == 0)
    {
    }
}
Run Code Online (Sandbox Code Playgroud)

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

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

int foo = intList[i];
Run Code Online (Sandbox Code Playgroud)

然后,变为慢约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();
}
Run Code Online (Sandbox Code Playgroud)

以下是结果:

生成数据.计算循环:2458ms计算foreach循环:2005ms

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

  • 你的foreach循环运行得更快,因为'for'会在每次迭代时评估条件.在您的示例中,这会产生一个额外的方法调用(获取list.count)简而言之,您正在对两个不同的代码进行基准测试,因此您的结果很奇怪.试试'int max = intlist.Count; for(int i = 0; i <max; i ++)...'和'for'循环将始终运行得更快,正如预期的那样! (8认同)
  • 使用秒表比使用DateTime.Now更好 - 而且我不相信任何快速运行,说实话. (6认同)

Tom*_*rst 9

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

如果list您正在使用的恰好是a LinkedList,则索引器代码(数组样式访问)的性能比使用IEnumeratorfrom foreach对于大型列表要差很多.

当您LinkedList使用索引器语法访问元素10.000时list[10000],链接列表将从头节点开始,并遍历Next-pointer一万次,直到它到达正确的对象.显然,如果你在循环中执行此操作,您将获得:

list[0]; // head
list[1]; // head.Next
list[2]; // head.Next.Next
// etc.
Run Code Online (Sandbox Code Playgroud)

当您调用GetEnumerator(隐式使用forach-syntax)时,您将获得一个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.
Run Code Online (Sandbox Code Playgroud)

正如您所看到的,在LinkedLists 的情况下,数组索引器方法变得越来越慢,循环的时间越长(它必须一遍又一遍地通过相同的头指针).而IEnumerable正义在不断的时间运作.

当然,正如Jon所说,这实际上取决于类型list,如果list不是a LinkedList,而是数组,那么行为就完全不同了.

  • .NET中的LinkedList没有索引器,因此它实际上不是一个选项. (4认同)