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

,以便比较遍历集合元素所需的时间,以及使用ListIEnumerable构建该集合是否更好。令我惊讶的是,List的结果是00:00.0005504,IEnumerable的结果是00:00.0016900。我期待第二种方式,IEnumerable,它会更快,因为值是在飞行中创建的,我们不需要每次添加它们中的每一个,就像List的情况一样,然后遍历它。

谁能给我解释一下这两者的区别吗?为什么我们有这种行为而没有相反的行为。

提前感谢您的帮助!

List<T> vs IEnumerable<T>

首先,您的测试方式并不能真正给您提供性能差异的有用印象。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  

可以看到,前者对CurrentMoveNext调用使用call,而后者使用callvirt。这是因为一个是在不能继承的List<T>.Enumerator上迭代,另一个是在必须考虑继承的IEnumerator<T>上迭代(例如,你可以返回你自己的枚举器,它实际上继承了另一个枚举器)。

关于callcallvirt的进一步阅读:call和callvirt。