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


105

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

1。

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

2。

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

31
我想这并不重要。如果您遇到性能问题,则几乎可以肯定不是由于这个原因。并不是说你不应该问这个问题……
darasd

2
除非您的应用程序对性能至关重要,否则我不会为此担心。拥有干净且易于理解的代码会更好。
Fortyrunner,2009年

2
令我担心的是,这里的一些答案似乎是由那些根本没有迭代器概念的人发布的,因此他们没有枚举器或指针的概念。
Ed James

3
该第二代码将无法编译。System.Object没有名为“值”的成员(除非您真的很邪恶,将其定义为扩展方法并比较委托)。强烈键入您的foreach。
Trillian

1
第一个代码也不会编译,除非true的类型list确实有一个count不是的成员Count
乔恩·斯基特

Answers:


130

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

它是否有意义,将取决于您是否在循环中进行任何实际工作。在几乎所有情况下,性能上的差异都不是很大,但可读性上的差异则有利于foreach循环。

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

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

编辑:对于那些声称对List<T>with进行迭代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规范中记录的内容...


乔恩不感兴趣,上面List <T>的情况...是否也适用于其他集合?另外,您是怎么知道这个的(没有恶意的意思)……就像您以前在尝试回答这个问题时确实偶然发现了这个吗?就是这样……随机/秘密:)
Pure.Krome

5
我已经了解了一段时间的数组优化-数组是一种“核心”集合;C#编译器已经非常了解它们,因此以不同的方式对待它们是有意义的。编译器不(也不应该)具有的任何特殊知识List<T>
乔恩·斯基特

干杯:)和是的……数组是我几年前在uni ..中学到的第一个集合概念,因此可以看出编译器足够聪明,可以处理一种(如果不是)最原始的类型。采集。再次欢呼!
Pure.Krome

3
@JonSkeet在迭代过程中修改列表时,优化列表迭代器可以改变行为。如果修改,则会丢失异常。仍然可以进行优化,但是需要检查是否未进行任何修改(我认为包括其他线程在内)。
Craig Gidney 2010年

5
@VeeKeyBee:微软在2004年说过。b)该作品将不得不做微小每次迭代大量工作,为这是显著。注意,无论如何,foreach在数组上等效for始终始终为提高可读性编写代码,然后只有在有证据表明它可以带来可衡量的性能优势时才进行微优化。
乔恩·斯基特

15

一个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;
}

如您所见,在循环中多次调用此方法比使用可以记住它在列表中位置的枚举器要慢得多。


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,for为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();
}

结果如下:

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

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


6
最好使用Stopwatch而不是DateTime.Now-老实说,我不会相信这么快的运行。
乔恩·斯基特

8
您的foreach循环运行得更快,因为“ for”每次迭代都会评估条件。在您的示例中,这需要一个额外的方法调用(以获取list.count)。简而言之,您正在对两个不同的代码进行基准测试,因此结果很奇怪。尝试'int max = intlist.Count; for(int i = 0; i <max; i ++)...'',并且“ for”循环将始终按预期运行!
AR

1
编译后,for和foreach在使用基元时会优化为完全相同的对象。直到您介绍List <T>为止,它们的速度(完全)不同。
Anthony Russell

9

注意:这个答案比Java适用于Java的要多得多,因为C#上没有索引器LinkedLists,但是我认为一般意义仍然成立。

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

当您LinkedList使用indexer语法:来访问元素10.000时list[10000],链表将从头节点开始,并遍历Next-pointer一千次,直到到达正确的对象。显然,如果您循环执行此操作,则会得到:

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

调用时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.

如您所见,在使用LinkedLists 的情况下,循环索引的方法变得越来越慢,循环的时间也越来越长(它必须一遍又一遍地通过相同的头指针)。尽管IEnumerable只是工作在固定的时间。

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


4
.NET中的LinkedList没有索引器,因此实际上不是一个选择。
乔恩·斯基特

哦,那很好解决了这个问题,然后:-)我只是在看LinkedList<T>MSDN上的文档,它有一个相当不错的API。最重要的是,它没有get(int index)像Java这样的方法。不过,我想对于其他任何类似列表的数据结构来说,这一点仍然成立,因为该数据结构公开的索引器比特定的慢IEnumerator
汤姆·洛霍斯特

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>此处,则仍然存在调用索引器的命中(可能是内联的)。这不像是裸机阵列访问。
乔恩·斯基特

非常真实!这是另一项财产执行,我们受实施的左右。
Charles Prakash Dasari

1

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

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

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


0

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

这使我得出一个有趣的结果,即foreach的速度比调试模式下的要快。而在释放模式下,for的速度比foreach的速度快。显然,与foreach循环相比,编译器具有更好的方法来优化for循环,因为foreach循环会损害多个方法调用。for循环是如此基础,以至于有可能甚至由CPU本身对其进行优化。


0

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

除非循环已展开(每步1.0个循环),否则标准foreach构造比简单的for-loop(每步2个循环)更快(每步1.5个循环)。

因此,对于日常的代码,性能是不是一个理由使用更复杂的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
您可能会重新阅读链接的代码项目文章。这是一篇有趣的文章,但与您的帖子恰恰相反。同样,您重新创建的表正在评估直接访问数组和List或通过其IList接口访问List的性能。两者都与这个问题无关。:)
Paul Walls 2014年

0

您可以在Deep .NET-第1部分迭代中了解它

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

例如-具有foreach循环的数组迭代: 在此处输入图片说明

和-使用foreach循环列出迭代: 在此处输入图片说明

最终结果: 在此处输入图片说明

在此处输入图片说明

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.