34

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

这让我想知道是否有任何理由无论是性能还是易用性,都可以在其他方面使用。还是仅仅取决于个人喜好。

取一个简单的对象

var myList = List<MyObject>; 

让我们假设对象已被填充并且我们想要遍历这些项目。

方法一。

foreach(var item in myList) 
{
   //Do stuff
}

方法二

myList.Foreach(ml => 
{
   //Do stuff
});

方法三

while (myList.MoveNext()) 
{
  //Do stuff
}

方法四

for (int i = 0; i < myList.Count; i++)
{
  //Do stuff   
}

我想知道的是这些中的每一个都编译成同一件事吗?使用其中一个是否有明显的性能优势?

还是这只是编码时的个人喜好?

我错过了吗?

4

2 回答 2

67

大多数时候的答案是没关系。 循环中的项目数量(甚至可能认为是“大量”项目,比如数千个)不会对代码产生影响。

当然,如果您将此确定为您的情况的瓶颈,请务必解决它,但您必须首先确定瓶颈。

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

让我们先定义几件事:

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

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

MyObject班级:

public class MyObject
{
    public int IntValue { get; set; }
    public double DoubleValue { get; set; }
}

创建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();
}

为列表中的每个项目执行的操作(需要,因为方法 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);
}

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

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

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

// The number of items to test.
public const int ItemsToTest = 1000000;

让我们进入方法:

方法一:foreach

以下代码:

foreach(var item in myList) 
{
   //Do stuff
}

编译成以下内容:

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

    // Do stuff.
}

那里发生了很多事情。你有方法调用(它可能会也可能不会反对IEnumerator<T>orIEnumerator接口,因为在这种情况下编译器尊重鸭子类型)并且你// 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);
    }
}

输出:

Foreach 循环滴答声:3210872841

方法2:.ForEach方法上List<T>

.ForEachon 方法的代码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]);
    }
}

请注意,这在功能上等同于方法 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);
    }
}

输出:

ForEach 方法记号:3135132204

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

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

方法三: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);
    }
}

输出:

枚举器循环刻度:3241289895

方法四:for

在这种特殊情况下,您将获得一些速度,因为列表索引器将直接进入底层数组以执行查找(这是一个实现细节,顺便说一句,没有什么可以说它不能是树结构备份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);
    }
}

输出:

列出索引器循环刻度: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);
    }
}

输出:

数组循环滴答声:3102911316

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


TL;博士

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

通常,您应该选择最可维护的方法,在这种情况下,方法 1 ( foreach) 是要走的路。

于 2013-03-06T12:44:31.173 回答
4

关于问题的最后一点,“我错过了吗?” 是的,我觉得即使这个问题已经很老了,我也不提这个问题。虽然这四种方法将在相对相同的时间内执行,但上面未显示的一种方法运行速度比所有方法都快。事实上,随着迭代列表中项目数量的增加,这一点非常重要。这将与最后一种方法完全相同,但不是进入.Count循环的条件检查,而是在设置循环之前将此值分配给一个变量并改用它。这给你留下了这样的东西:

var countVar = list.Count;
for(int i = 0; i < countVar; i++)
{
 //loop logic
}

通过这种方式,您只需在每次迭代时查找一个变量值,而不是解析 Count 或 Length 属性,这会大大降低效率。

于 2019-07-25T15:40:57.850 回答