c#中最有效的循环是什么

The*_*ear 28 c# foreach loops for-loop while-loop

有许多不同的方法可以通过c#中的对象项来完成相同的简单循环.

这让我想知道是否有任何理由表现或使用方便,以至于在另一方面使用.或者仅仅是个人偏好.

拿一个简单的对象

var myList = List<MyObject>; 
Run Code Online (Sandbox Code Playgroud)

让我们假设对象已填充,我们想迭代这些项目.

方法1.

foreach(var item in myList) 
{
   //Do stuff
}
Run Code Online (Sandbox Code Playgroud)

方法2

myList.Foreach(ml => 
{
   //Do stuff
});
Run Code Online (Sandbox Code Playgroud)

方法3

while (myList.MoveNext()) 
{
  //Do stuff
}
Run Code Online (Sandbox Code Playgroud)

方法4

for (int i = 0; i < myList.Count; i++)
{
  //Do stuff   
}
Run Code Online (Sandbox Code Playgroud)

我想知道的是,每个编译成同样的东西?使用一个比其他人有明显的性能优势吗?

或者这只是编码时的个人偏好?

我错过了吗?

cas*_*One 55

大多数时候答案是无关紧要的. 循环中的项目数量(即使是人们可能认为的"大"项目数量,比如成千上万)也不会对代码产生影响.

当然,如果你认为这是你的情况的瓶颈,一定要解决它,但你必须首先确定瓶颈.

也就是说,每种方法都需要考虑很多事情,我将在此概述.

我们首先定义一些事情:

  • 所有测试都在32位处理器上的.NET 4.0上运行.
  • TimeSpan.TicksPerSecond 在我的机器上= 10,000,000
  • 所有测试都在单独的单元测试会话中执行,而不是在同一个测试会话中(以免可能干扰垃圾收集等)

以下是每个测试所需的一些帮助:

MyObject类:

public class MyObject
{
    public int IntValue { get; set; }
    public double DoubleValue { get; set; }
}
Run Code Online (Sandbox Code Playgroud)

一种创建List<T>任意长度MyClass实例的方法:

public static List<MyObject> CreateList(int items)
{
    // Validate parmaeters.
    if (items < 0) 
        throw new ArgumentOutOfRangeException("items", items, 
            "The items parameter must be a non-negative value.");

    // Return the items in a list.
    return Enumerable.Range(0, items).
        Select(i => new MyObject { IntValue = i, DoubleValue = i }).
        ToList();
}
Run Code Online (Sandbox Code Playgroud)

要对列表中的每个项执行的操作(需要,因为方法2使用委托,并且需要调用某些内容来测量影响):

public static void MyObjectAction(MyObject obj, TextWriter writer)
{
    // Validate parameters.
    Debug.Assert(obj != null);
    Debug.Assert(writer != null);

    // Write.
    writer.WriteLine("MyObject.IntValue: {0}, MyObject.DoubleValue: {1}", 
        obj.IntValue, obj.DoubleValue);
}
Run Code Online (Sandbox Code Playgroud)

一种创建TextWriter写入null Stream(基本上是数据接收器)的方法:

public static TextWriter CreateNullTextWriter()
{
    // Create a stream writer off a null stream.
    return new StreamWriter(Stream.Null);
}
Run Code Online (Sandbox Code Playgroud)

让我们将项目数量修正为一百万(1,000,000,这应该足够高以强制执行通常,这些都具有相同的性能影响):

// The number of items to test.
public const int ItemsToTest = 1000000;
Run Code Online (Sandbox Code Playgroud)

让我们进入方法:

方法1: foreach

以下代码:

foreach(var item in myList) 
{
   //Do stuff
}
Run Code Online (Sandbox Code Playgroud)

编译如下:

using (var enumerable = myList.GetEnumerable())
while (enumerable.MoveNext())
{
    var item = enumerable.Current;

    // Do stuff.
}
Run Code Online (Sandbox Code Playgroud)

那里有很多东西.你有方法调用(它可能或可能不是针对IEnumerator<T>IEnumerator接口,因为编译器在这种情况下尊重鸭子类型)并且你// Do stuff被提升到那个while结构中.

以下是衡量绩效的测试:

[TestMethod]
public void TestForEachKeyword()
{
    // Create the list.
    List<MyObject> list = CreateList(ItemsToTest);

    // Create the writer.
    using (TextWriter writer = CreateNullTextWriter())
    {
        // Create the stopwatch.
        Stopwatch s = Stopwatch.StartNew();

        // Cycle through the items.
        foreach (var item in list)
        {
            // Write the values.
            MyObjectAction(item, writer);
        }

        // Write out the number of ticks.
        Debug.WriteLine("Foreach loop ticks: {0}", s.ElapsedTicks);
    }
}
Run Code Online (Sandbox Code Playgroud)

输出:

Foreach循环滴答:3210872841

方法2:.ForEach方法开启List<T>

.ForEach方法的代码List<T>看起来像这样:

public void ForEach(Action<T> action)
{
    // Error handling omitted

    // Cycle through the items, perform action.
    for (int index = 0; index < Count; ++index)
    {
        // Perform action.
        action(this[index]);
    }
}
Run Code Online (Sandbox Code Playgroud)

请注意,这在功能上等同于方法4,但有一个例外,提升到for循环中的代码作为委托传递.这需要取消引用才能获得需要执行的代码.虽然代表的性能已经从.NET 3.0开始有所提升,但这种开销仍然存在.

但是,它可以忽略不计.衡量绩效的测试:

[TestMethod]
public void TestForEachMethod()
{
    // Create the list.
    List<MyObject> list = CreateList(ItemsToTest);

    // Create the writer.
    using (TextWriter writer = CreateNullTextWriter())
    {
        // Create the stopwatch.
        Stopwatch s = Stopwatch.StartNew();

        // Cycle through the items.
        list.ForEach(i => MyObjectAction(i, writer));

        // Write out the number of ticks.
        Debug.WriteLine("ForEach method ticks: {0}", s.ElapsedTicks);
    }
}
Run Code Online (Sandbox Code Playgroud)

输出:

ForEach方法刻度:3135132204

实际上比使用循环约7.5秒foreach.并不完全令人惊讶,因为它使用直接阵列访问而不是使用IEnumerable<T>.

但请记住,每个项目的保存时间为0.0000075740637秒.对于小项目列表来说,这值得.

方法3: while (myList.MoveNext())

如方法1所示,这正是编译器所做的(添加using语句,这是一种很好的做法).你不是通过自己解开编译器会产生的代码来获得任何东西.

对于踢球,我们无论如何都要这样做:

[TestMethod]
public void TestEnumerator()
{
    // Create the list.
    List<MyObject> list = CreateList(ItemsToTest);

    // Create the writer.
    using (TextWriter writer = CreateNullTextWriter())
    // Get the enumerator.
    using (IEnumerator<MyObject> enumerator = list.GetEnumerator())
    {
        // Create the stopwatch.
        Stopwatch s = Stopwatch.StartNew();

        // Cycle through the items.
        while (enumerator.MoveNext())
        {
            // Write.
            MyObjectAction(enumerator.Current, writer);
        }

        // Write out the number of ticks.
        Debug.WriteLine("Enumerator loop ticks: {0}", s.ElapsedTicks);
    }
}
Run Code Online (Sandbox Code Playgroud)

输出:

枚举器循环滴答:3241289895

方法4: for

在这种特殊情况下,你将获得一些速度,因为列表索引器直接进入底层数组来执行查找(这是一个实现细节,BTW,没有什么可说的,它不能是树结构支持List<T>起来).

[TestMethod]
public void TestListIndexer()
{
    // Create the list.
    List<MyObject> list = CreateList(ItemsToTest);

    // Create the writer.
    using (TextWriter writer = CreateNullTextWriter())
    {
        // Create the stopwatch.
        Stopwatch s = Stopwatch.StartNew();

        // Cycle by index.
        for (int i = 0; i < list.Count; ++i)
        {
            // Get the item.
            MyObject item = list[i];

            // Perform the action.
            MyObjectAction(item, writer);
        }

        // Write out the number of ticks.
        Debug.WriteLine("List indexer loop ticks: {0}", s.ElapsedTicks);
    }
}
Run Code Online (Sandbox Code Playgroud)

输出:

列表索引器循环标记:3039649305

然而,这可以产生影响的地方是数组.编译器可以解开数组以一次处理多个项目.

编译器可以在十个项目循环中将其展开为两个项目的五次迭代,而不是在十个项目循环中对一个项目进行十次迭代.

但是,我在这里并不认为这实际上正在发生(我必须查看IL和编译的IL的输出).

这是测试:

[TestMethod]
public void TestArray()
{
    // Create the list.
    MyObject[] array = CreateList(ItemsToTest).ToArray();

    // Create the writer.
    using (TextWriter writer = CreateNullTextWriter())
    {
        // Create the stopwatch.
        Stopwatch s = Stopwatch.StartNew();

        // Cycle by index.
        for (int i = 0; i < array.Length; ++i)
        {
            // Get the item.
            MyObject item = array[i];

            // Perform the action.
            MyObjectAction(item, writer);
        }

        // Write out the number of ticks.
        Debug.WriteLine("Enumerator loop ticks: {0}", s.ElapsedTicks);
    }
}
Run Code Online (Sandbox Code Playgroud)

输出:

数组循环滴答:3102911316

应该注意的是,开箱即用,Resharper提供了一个重构建议,将上述for语句更改为foreach语句.这并不是说这是对的,但基础是减少代码中的技术债务数量.


TL; DR

你真的不应该关心这些东西的表现,除非在你的情况下测试表明你有一个真正的瓶颈(并且你必须有大量的项目才能产生影响).

一般来说,你应该选择最可维护的东西,在这种情况下,方法1(foreach)是要走的路.

  • @KenKin我要说的是TL; DR **强调*这是*必须*阅读的部分。而且,这是最重要的内容的重复。 (2认同)
  • @KenKin只是为了清楚起见,TL; DR **用于声明“如果您不想阅读所有其他文本,则应阅读该文本。” (2认同)