List<T> vs IEnumerable<T>
本文关键字:gt lt IEnumerable vs List | 更新日期: 2023-09-27 17:51:21
我运行了以下控制台应用程序:
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.0005504,IEnumerable
的结果是00:00.0016900。我期待第二种方式,IEnumerable
,它会更快,因为值是在飞行中创建的,我们不需要每次添加它们中的每一个,就像List
的情况一样,然后遍历它。
提前感谢您的帮助!
首先,您的测试方式并不能真正给您提供性能差异的有用印象。1万个项目的迭代实在太短了;当你得到以微秒为单位的结果时,你已经可以看到了。相反,你应该尽量多花几秒钟的时间。此外,您应该总是按顺序运行相同的测试多次,然后从中取平均值。这样你就可以消除随机影响,得到更稳定的结果(另见大数定律)。
但是,迭代生成器函数可能比迭代列表要慢。这是出于不同的原因:首先,当您从暂停其执行的函数中获取项时,实际上会导致大量上下文切换。我不确定这对生成器函数有多优化,但你仍然需要以某种方式处理它们,所以你确实有一个惩罚。
第二,列表在内部使用数组,数组可以根据需要动态调整大小。所以最后,当你在列表上迭代时,你就是在数组上迭代。你正在对内存中的一组数字进行迭代。这将总是比其他任何操作都快。
最大的区别是内存方面,这是应该让您考虑生成器函数而不是完整列表的原因。当您创建列表时,您将快速生成所有项,将它们放入内存,然后再次快速遍历它们。但你也把它们全部放入内存。因此,根据物品的数量,这可能意味着很大的成本。特别是当你只需要访问一个项目一次,这往往是不值得的。另一方面,生成器函数只需要单个项目的内存,所以在内存方面,这是非常有效的。
最后,虽然存在速度差异,但这可能不会有太大影响。很少有应用程序因为您决定在某处使用生成器函数而变慢的情况。更有可能的是,应用程序的瓶颈在其他地方,最有可能在I/O或网络操作中,所以在它成为问题之前,您真的不应该关心它。
答案很简单。
列表使用大量内存,这会使缓存过载。该方法不会使用大量内存,因此在处理器的第一级缓存中运行。至少有一个可能的解释。
差异还可能是因为下面使用了不同的枚举数。例如,用于枚举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
可以看到,前者对Current
和MoveNext
调用使用call
,而后者使用callvirt
。这是因为一个是在不能继承的List<T>.Enumerator
上迭代,另一个是在必须考虑继承的IEnumerator<T>
上迭代(例如,你可以返回你自己的枚举器,它实际上继承了另一个枚举器)。
关于call
和callvirt
的进一步阅读:call和callvirt。