我运行了以下控制台应用程序:
class Program
{
static void Main(string[] args)
{
int n = 10000;
Stopwatch s = new Stopwatch();
s.Start();
List<int> numbers = GetListNumber(n);
foreach (var number in numbers)
{
}
s.Stop();
Console.WriteLine(s.Elapsed);
Console.WriteLine();
s.Restart();
foreach (var number in GetEnumerator(n))
{
}
s.Stop();
Console.WriteLine(s.Elapsed);
Console.ReadKey();
}
static List<int> GetListNumber(int n)
{
List<int> numbers = new List<int>();
for (int i = 0; i < n; i++)
numbers.Add(i);
return numbers;
}
static IEnumerable<int> GetEnumerator(int n)
{
for (int i = 0; i < n; i++)
yield return i;
}
}
为了比较我们需要迭代集合元素的时间,以及使用List
或IEnumerable
构建此集合是否更好。令我惊讶的是,结果是List
的00:00:00.0005504和IEnumerable
的00:00:00.0016900。我期待第二种方式IEnumerable
,它会更快,因为这些值是动态创建的,我们不必每次都为它们添加一个项目,就像在a List
然后遍历它。
提前感谢您的帮助!
答案 0 :(得分:4)
首先,您测试的方式实际上无法为您提供有用的性能差异印象。 10000件物品的迭代实在太短了;你可以看到这个,因为你有微秒的结果。相反,你应该总是尝试从它获得多秒。此外,您应该始终按顺序运行相同的测试多个次,然后取出它的平均值。这样您就可以消除随机影响并获得更稳定的结果(另请参阅law of large numbers)。
但是,是的,迭代生成器函数可能比列表慢。这是出于不同的原因:首先,当你从暂停执行的函数中获取项目时,你最终会得到很多上下文切换。我不确定这对于生成器函数有多优化,但你仍然需要以某种方式处理它们,所以你确实在那里有一个阴谋。
其次,列表内部使用根据需要动态调整大小的数组。所以最后,当你迭代一个列表时,你正在遍历一个数组。您正在迭代内存中的一系列数字。这总是比其他任何东西都快。
最重要的区别是内存方面,它应该让你考虑生成器功能而不是完整列表。创建列表时,您正在快速生成所有项目,将它们放入内存,然后再次快速迭代它们。但是你也将它们 all 放入内存中。因此,根据项目数量,这可能意味着很大的成本。特别是当你只需要访问一次项目时,这通常是不值得的。另一方面,生成器函数只需要单个项目的内存,因此在内存方面,这非常有效。
最后,虽然存在速度差异,但这可能永远不会有太大影响。由于您决定在某处使用生成器函数,因此应用程序的速度很慢。更有可能的是,您的应用程序的瓶颈在于其他地方,最有可能在I / O或网络操作中,所以在它成为问题之前,您真的不应该关心它。
答案 1 :(得分:0)
简单回答。
该列表使用大量内存,这会使缓存超载。该方法不会使用大量内存,因此可以在处理器的第一级缓存中运行。至少有一种可能的解释。特别是当你做了超过1000个数字时。
答案 2 :(得分:0)
差异可能也是因为下面使用了不同的枚举器。例如,用于枚举List<T>
的IL如下所示:
callvirt System.Collections.Generic.List<System.Int32>.GetEnumerator
stloc.s 04 // CS$5$0000
br.s IL_0030
ldloca.s 04 // CS$5$0000
call System.Collections.Generic.List<System.Int32>+Enumerator.get_Current
stloc.3 // number
nop
nop
ldloca.s 04 // CS$5$0000
call System.Collections.Generic.List<System.Int32>+Enumerator.MoveNext
stloc.s 05 // CS$4$0001
ldloc.s 05 // CS$4$0001
brtrue.s IL_0026
leave.s IL_004E
ldloca.s 04 // CS$5$0000
constrained. System.Collections.Generic.List<>.Enumerator
callvirt System.IDisposable.Dispose
nop
endfinally
IL用于迭代IEnumerable<T>
,如下所示:
callvirt System.Collections.Generic.IEnumerable<System.Int32>.GetEnumerator
stloc.s 06 // CS$5$0002
br.s IL_008E
ldloc.s 06 // CS$5$0002
callvirt System.Collections.Generic.IEnumerator<System.Int32>.get_Current
stloc.3 // number
nop
nop
ldloc.s 06 // CS$5$0002
callvirt System.Collections.IEnumerator.MoveNext
stloc.s 05 // CS$4$0001
ldloc.s 05 // CS$4$0001
brtrue.s IL_0084
leave.s IL_00B1
ldloc.s 06 // CS$5$0002
ldnull
ceq
stloc.s 05 // CS$4$0001
ldloc.s 05 // CS$4$0001
brtrue.s IL_00B0
ldloc.s 06 // CS$5$0002
callvirt System.IDisposable.Dispose
nop
endfinally
正如您所看到的,前者使用call
进行Current
和MoveNext
调用,但后者使用callvirt
。这是因为一个正在迭代List<T>.Enumerator
无法继承,另一个正在使用IEnumerator<T>
,其中还必须考虑继承(例如,您可以返回自己的枚举器,它实际上将继承自另一个)。
可以对call
vs callvirt
进行进一步阅读:call and callvirt。