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

本文关键字:循环 是什么 有效 | 更新日期: 2023-09-27 18:02:26

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

这让我想知道是否有任何原因,无论是性能还是易用性,使用on而不是其他。还是说这只是个人喜好。

取一个简单对象

var myList = List<MyObject>; 

假设对象已经填充,我们想要遍历这些项。

1 .

方法

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   
}

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

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

我错过了什么吗?

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

大多数时候的答案是没关系。循环中的项数(甚至可能被认为是"large")条目的数量(比如以千为单位)不会对代码产生影响。

当然,如果您认为这是您的情况中的瓶颈,那么尽一切办法解决它,但您必须首先确定瓶颈。

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

让我们先定义一些东西:

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

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

MyObject类:

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

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

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的方法,它写一个空的Stream(基本上是一个数据接收器):

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

让我们将项目的数量固定为一百万(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>IEnumerator接口,也可能不违反,因为编译器在这种情况下尊重duck-typing),并且您的// 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循环ticks: 3210872841

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

List<T>.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 method ticks: 3135132204

实际上 ~比使用foreach循环快7.5秒。考虑到它使用直接数组访问而不是使用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)是可行的。

关于问题的最后一点,"我有遗漏什么吗?"是的,我觉得不提这个问题是我的疏忽,尽管这个问题很老了。虽然这四种方法的执行时间相对相同,但有一种方法比所有方法都快。实际上,随着迭代列表中项目数量的增加,这一点非常明显。这将是与上一个方法完全相同的方式,但不是在循环的条件检查中获得.Count,而是在设置循环之前将该值赋给一个变量并使用它。剩下的是这样的内容:

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

通过这种方式,您只在每次迭代中查找一个变量值,而不是解析Count或Length属性,后者效率相当低。

我建议一种更好但不为人所知的方法来加快列表的循环迭代。我建议你先阅读Span<T>。请注意,如果您正在使用.NET Core,则可以使用它。

List<MyObject> list = new();
foreach (MyObject item in CollectionsMarshal.AsSpan(list))
{
    // Do something
}

注意注意事项:

CollectionsMarshal.AsSpan方法是不安全的,只有当你知道你在做什么时才应该使用。CollectionsMarshal.AsSpan返回List<T>私有数组上的Span<T>。在Span<T>上迭代速度很快,因为JIT使用与优化数组相同的技巧。使用此方法,它不会在枚举期间检查列表是否被修改。

这是一个更详细的解释它在幕后做什么和更多,超级有趣!