c#中最有效的循环是啥

Posted

技术标签:

【中文标题】c#中最有效的循环是啥【英文标题】:What is the most efficient loop in c#c#中最有效的循环是什么 【发布时间】:2013-02-21 05:42:48 【问题描述】:

有许多不同的方法可以通过 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   

我想知道的是把这些都编译成同一个东西吗?使用其中一种是否具有明显的性能优势?

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

我错过了吗?

【问题讨论】:

如果您想知道它们在性能上的差异,您可以随时进行自己的基准测试。 System.Diagnostics.StopWatch是你的朋友 看到这个:forums.asp.net/t/1041090.aspx 你错过了 do .. while()。除此之外,为什么不直接编译它们并在 ILDASM 中查看结果呢? 您是否对性能进行了基准测试?你的结果说明了什么?很大程度上,您自己的任何逻辑都将成为瓶颈,您的迭代方式在很大程度上可以忽略不计,有时还取决于情况。有点无关紧要的纯学术问题。 使用最易读的(对你来说)和你需要的。因此,如果您想稍后更改实现,请将List&lt;MyObject&gt; 更改为IEnumerable&lt;MyObject&gt; 并使用foreach,或者如果您不需要索引。我必须承认,在过去的 10 年里,我从来没有使用过List.ForEach,而不仅仅是关于 SO 的答案。 【参考方案1】:

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

var countVar = list.Count;
for(int i = 0; i < countVar; i++)

 //loop logic

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

【讨论】:

我非常同意这个分析。缓存列表的长度可以减少完成时间并提高效率。【参考方案2】:

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

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

也就是说,每种方法都有许多需要考虑的因素,我将在此处概述。

让我们先定义一些东西:

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

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

MyObject 类:

public class MyObject

    public int IntValue  get; set; 
    public double DoubleValue  get; set; 

一种创建List&lt;T&gt; 任意长度的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 使用委托,并且需要调用 something 来衡量影响):

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 的方法,该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&lt;T&gt;IEnumerator 接口,因为在这种情况下编译器尊重鸭子类型)并且你的// Do stuff 被提升到该结构中。

这是衡量性能的测试:

[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:.ForEachList&lt;T&gt; 上的方法

List&lt;T&gt; 上的 .ForEach 方法的代码如下所示:

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

实际上比使用 foreach 循环快 7.5 秒。这并不奇怪,因为它使用直接数组访问而不是使用IEnumerable&lt;T&gt;

但请记住,这意味着每个项目的保存时间为 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&lt;T&gt; 向上的树结构。

[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;DR

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

一般来说,您应该选择最可维护的方法,在这种情况下,方法 1 (foreach) 是可行的方法。

【讨论】:

+1 很好的答案!但我觉得tl;dr部分本身不应该,其实是很重要的结论。 啊..我觉得不应该是tl;dr,很重要,必读 @KenKin 我想说TL;DR 强调那是 必须 阅读的部分。此外,它是顶部内容的重复。 @KenKin 明确地说,TL;DR 用于声明“如果您不想阅读所有其他文本,这就是您应该阅读的内容。 " 干得好,有趣的是,从 Chris Gessler 之前提供的链接来看,使用 Linq 看起来比 foreach() 慢,但他们在测试 .Any() 和 .TakeWhile(),但是从您的工作中可以看出,linq 结果是最快的,而不是移至 for()。再次感谢您的出色工作。

以上是关于c#中最有效的循环是啥的主要内容,如果未能解决你的问题,请参考以下文章

Java中循环局部变量初始化的最有效方法是啥? [复制]

构建非循环依赖关系的最简单和最有效的数据结构是啥?

在不对所有记录执行循环的情况下从数据库中检索特定数据的最有效方法是啥?

将 Enumeration<Integer> for 循环从 Java 转换为 C#? C# 中的 Enumeration<Integer> 到底是啥? [复制]

while循环结构的语法和执行顺序是啥?

内存如何帮助 C# 上的巨大循环