列出<t> vs IEnumerable <t> </t> </t>

时间:2014-03-23 15:30:30

标签: c#

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

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:00.0005504和IEnumerable的00:00:00.0016900。我期待第二种方式IEnumerable,它会更快,因为这些值是动态创建的,我们不必每次都为它们添加一个项目,就像在a List然后遍历它。

可以请别人解释一下这个区别吗?为什么我们得到这种行为,而不是相反的行为。

提前感谢您的帮助!

3 个答案:

答案 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进行CurrentMoveNext调用,但后者使用callvirt。这是因为一个正在迭代List<T>.Enumerator无法继承,另一个正在使用IEnumerator<T>,其中还必须考虑继承(例如,您可以返回自己的枚举器,它实际上将继承自另一个)。

可以对call vs callvirt进行进一步阅读:call and callvirt