大多数时候的答案是没关系。循环中的项目数(甚至可能认为是“大量”项目,比如数千个)是不会对代码产生影响。
当然,如果你认为这是你的情况的瓶颈,一定要解决它,但你必须首先确定瓶颈。
也就是说,每种方法都有许多需要考虑的因素,我将在此处概述。
让我们先定义一些东西:
- 所有测试均在 32 位处理器上的 .NET 4.0 上运行。
-
TimeSpan.TicksPerSecond 在我的机器上 = 10,000,000
- 所有测试都在单独的单元测试会话中执行,而不是在同一个会话中(以免干扰垃圾收集等)
以下是每个测试所需的一些帮助器:
MyObject 类:
public class MyObject
{
public int IntValue { get; set; }
public double DoubleValue { get; set; }
}
一种创建List<T> 任意长度的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<T> 或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<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 方法记号: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;DR
你真的不应该关心这些东西的性能,除非在你的情况下的测试表明你有一个真正的瓶颈(你必须有大量的项目才能产生影响)。
一般来说,您应该选择最可维护的方法,在这种情况下,方法 1 (foreach) 是可行的方法。