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. } 

那里有很多东西。 你有方法调用(它可能会或可能不会对IEnumeratorIEnumerator接口,因为编译器在这种情况下尊重鸭子类型)和你的// 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 )是要走的路。