通过c#中对象的项目,有许多不同的方法来完成相同的简单循环。
这让我想知道是否有任何理由表现或使用方便,以至于在另一方面使用。或者仅仅是个人偏好。
拿一个简单的物体
var myList = List<MyObject>;
让我们假设对象已填满,我们想迭代这些项目。
方法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
}
我想知道的是,每个编译成同样的东西吗?使用一个比其他人有明显的性能优势吗?
或者这仅仅是编码时的个人偏好?
我错过了吗?
答案 0 :(得分:49)
大部分时间的答案是无关紧要。循环中的项目数量(即使是人们可能认为是“大”项目的数量,比如成千上万)不会对代码产生影响。
当然,如果你认为这是你情况的瓶颈,一定要解决它,但你必须首先确定瓶颈。
尽管如此,每种方法都需要考虑很多事项,我将在此概述。
让我们首先定义一些事情:
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
并写入null 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
接口,因为编译器在这种情况下尊重鸭子类型)并且您的// 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
.ForEach
List<T>
方法
.ForEach
上List<T>
方法的代码如下所示:
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
实际 ~7.5秒更快比使用foreach
循环更快。并不完全令人惊讶,因为它使用直接数组访问而不是使用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
在这种特殊情况下,你会获得一些速度,因为列表索引器直接进入底层数组来执行查找(这是一个实现细节,BTW,没有什么可说的,它不能支持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
)是可行的方法。
答案 1 :(得分:1)
关于问题的最后一点,“我错过了吗?”是的,即使这个问题已经很老了,我还是不肯在这里不提。尽管这四种方法将在相对相同的时间内执行,但它们的运行速度却比所有方法都要快,但实际上这是非常重要的,因为要迭代的列表的大小会增加。它将与最后一种方法完全相同,但是不会在循环的条件检查中获取.Count,而是在设置循环之前将此值分配给变量,然后使用它,从而给您留下类似的东西
var countVar = list.Count;
for(int i = 0; i < countVar; i++)
{
//loop logic
}
这样,您只需在每次迭代中查找一个变量值,而不是解析Count或Length属性,这会大大降低效率。