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
}
我想知道的是,这些都编译成相同的东西吗?使用其中一个是否有明显的性能优势?
或者这只是编码时的个人偏好?
我错过了什么吗?
大多数时候的答案是没关系。循环中的项数(甚至可能被认为是"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
使用与优化数组相同的技巧。使用此方法,它不会在枚举期间检查列表是否被修改。
这是一个更详细的解释它在幕后做什么和更多,超级有趣!