c#中最有效的循环是什么
有许多不同的方法可以通过c#中的对象项来完成相同的简单循环。
这让我想知道是否有任何理由表现或使用方便,以至于使用在另一方面。 或者仅仅是个人偏好。
拿一个简单的对象
var myList = List;
让我们假设对象已填充,我们想迭代这些项目。
方法1。
foreach(var item in myList) { //Do stuff }
方法2
myList.Foreach(ml => { //Do stuff });
方法3
while (myList.MoveNext()) { //Do stuff }
方法4
for (int i = 0; i < myList.Count; i++) { //Do stuff }
我想知道的是,每个编译成同样的东西? 使用一个比其他人有明显的性能优势吗?
或者这只是编码时的个人偏好?
我错过了吗?
大多数时候答案是无关紧要的。 循环中的项目数量(即使是人们可能认为的“大”数量的项目,比如成千上万)也不会对代码产生影响。
当然,如果你认为这是你的情况的瓶颈,一定要解决它,但你必须首先确定瓶颈。
也就是说,每种方法都需要考虑很多事情,我将在此概述。
我们首先定义一些事情:
- 所有测试都在32位处理器上的.NET 4.0上运行。
- 我的机器上的
TimeSpan.TicksPerSecond
= 10,000,000 - 所有测试都在单独的unit testing会话中执行,而不是在同一个测试会话中(以免可能干扰垃圾收集等)
以下是每个测试所需的一些帮助:
MyObject
类:
public class MyObject { public int IntValue { get; set; } public double DoubleValue { get; set; } }
一种创建任何长度的MyClass
实例的List
的方法:
public static List 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
方法,该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;
让我们进入方法:
方法1: foreach
以下代码:
foreach(var item in myList) { //Do stuff }
编译如下:
using (var enumerable = myList.GetEnumerable()) while (enumerable.MoveNext()) { var item = enumerable.Current; // Do stuff. }
那里有很多东西。 你有方法调用(它可能会或可能不会对IEnumerator
或IEnumerator
接口,因为编译器在这种情况下尊重鸭子类型)和你的// Do stuff
被提升到那个while结构中。
以下是衡量绩效的测试:
[TestMethod] public void TestForEachKeyword() { // Create the list. List 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: List
上的.ForEach
方法
List
上的.ForEach
方法的代码如下所示:
public void ForEach(Action action) { // Error handling omitted // Cycle through the items, perform action. for (int index = 0; index < Count; ++index) { // Perform action. action(this[index]); } }
请注意,这在function上等同于方法4,但有一个例外,提升到for
循环中的代码作为委托传递。 这需要取消引用才能获得需要执行的代码。 虽然代表的性能已经从.NET 3.0开始提高,但这种开销仍然存在。
但是,它可以忽略不计。 衡量绩效的测试:
[TestMethod] public void TestForEachMethod() { // Create the list. List 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
。
但请记住,每个项目的保存时间为0.0000075740637秒。 对于小项目列表来说,这不值得。
方法3: while (myList.MoveNext())
如方法1所示,这正是编译器所做的(添加using
语句,这是一种很好的做法)。 你不是通过自己解开编译器会产生的代码来获得任何东西。
对于踢球,我们无论如何都要这样做:
[TestMethod] public void TestEnumerator() { // Create the list. List list = CreateList(ItemsToTest); // Create the writer. using (TextWriter writer = CreateNullTextWriter()) // Get the enumerator. using (IEnumerator 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
方法4: for
在这种特殊情况下,你将获得一些速度,因为列表索引器直接进入底层数组来执行查找(这是一个实现细节,BTW,没有什么可说的,它不能是树结构支持List
up)。
[TestMethod] public void TestListIndexer() { // Create the list. List 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提供了一个建议,通过重构将上述语句更改为foreach
语句。 这并不是说这是对的,但基础是减少代码中的技术债务数量。
TL; DR
你真的不应该关心这些东西的表现,除非在你的情况下测试表明你有一个真正的瓶颈(并且你必须有大量的项目才能产生影响)。
一般来说,你应该选择最可维护的东西,在这种情况下,方法1( foreach
)是要走的路。