代码之家  ›  专栏  ›  技术社区  ›  Christos

列表<T>与IEnumerable<T>

c#
  •  6
  • Christos  · 技术社区  · 12 年前

    我运行了以下控制台应用程序:

    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 。令我惊讶的是,结果是00:00:00.0005504 列表 对于 IE可编号 我以为第二种方式, IE可编号 ,这会更快,因为值是动态创建的,我们不必每次将它们添加到一个项目中,就像在 列表 然后遍历它。

    请有人给我解释一下这个区别好吗?为什么我们有这种行为而没有相反的行为。

    提前感谢您的帮助!

    3 回复  |  直到 12 年前
        1
  •  4
  •   poke    12 年前

    首先,您的测试方式并不能真正给您带来性能差异的有用印象。10000项的迭代实在太短;您已经可以看到这一点,因为您的结果以微秒为单位。相反,您应该总是尝试从中获得几秒钟的时间。此外,您应该始终运行相同的测试 倍数 然后从中取出平均值。这样可以消除随机影响并获得更稳定的结果(另请参见 law of large numbers ).

    但是的,迭代生成器函数可能会比列表慢。这是出于不同的原因:首先,当您从暂停执行的函数中获取项时,实际上会出现大量上下文切换。我不确定这对生成器函数有多优化,但您仍然必须以某种方式处理它们,因此您确实会受到惩罚。

    第二,列表内部使用数组,根据需要动态调整大小。所以最后,当您在列表上迭代时,您在数组上迭代。您正在对内存中的一系列数字进行迭代。这将永远比其他任何事情都快。

    最大的区别是内存方面,这应该使您考虑生成器函数而不是完整列表。创建列表时,您将快速生成所有项,将它们放入内存,然后再次快速迭代。但你也把它们 全部的 存储在内存中。因此,根据项目的数量,这可能意味着巨大的成本。特别是当你只需要访问一个项目一次时,这通常是不值得的。另一方面,生成器函数只需要一个项目的内存,所以从内存方面来说,这是非常有效的。

    最后,虽然存在速度差异,但这可能不会有多大影响。很少有应用程序会因为您决定在某处使用生成器函数而变慢。更可能的是,应用程序的瓶颈在其他地方,最有可能是在I/O或网络操作中,所以在它成为问题之前,您真的不应该关心它。

        2
  •  0
  •   TomTom    12 年前

    简单的答案。

    该列表使用大量内存,这会使缓存过载。该方法不会使用大量内存,因此在处理器的一级缓存中运行。至少有一种可能的解释。尤其是当你做了1000多个数字时。

        3
  •  0
  •   Community Mohan Dere    9 年前

    这种差异还可能是因为下面使用了不同的枚举器。例如,IL用于枚举 List<T> 如下所示:

    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> 其中还必须考虑继承(例如,您可以返回自己的枚举器,该枚举器实际上将从另一个枚举器继承)。

    进一步阅读 呼叫 callvirt公司 可以有: call and callvirt .