迭代遍历 IEnumerable<T> 和列表<T>之间的性能



今天,我在迭代条目列表时遇到了性能问题。在做了一些诊断之后,我终于找到了导致性能下降的原因。结果表明,迭代一个IEnumerable<T>比迭代一个List<T>要花费更多的时间。请帮助我理解为什么IEnumerable<T>List<T>慢。

UPDATE benchmark context:

我使用NHibernate从数据库中获取一组项目到IEnumerable<T>并求和其属性的值。这只是一个没有任何引用类型的简单实体:

public SimpleEntity
{
    public int Id {get;set}
    public string Name {get;set}
    public decimal Price {get;set}
}
Public Test
{
    void Main()
    {
        //this query get a list of about 200 items
        IEnumerable<SimpleEntity> entities = from entity in Session.Query<SimpleEntity>
                                             select entity;
        decimal value = 0.0;
        foreach(SimpleEntity item in entities)
        {
             //this for loop took 1.5 seconds 
             value += item.Price;
        }
        List<SimpleEntity> lstEntities = entities.ToList();
        foreach(SimpleEntity item in lstEntities)
        {
             //this for loop took less than a milisecond
             value += item.Price;
        }
    }
}

枚举一个IEnumerable<T>的速度是直接枚举同一个List<T>的2 ~ 3倍。这是由于c#在为给定类型选择枚举数时的微妙之处。

List<T>公开3个枚举数:

  1. List<T>.Enumerator List<T>.GetEnumerator()
  2. IEnumerator<T> IEnumerable<T>.GetEnumerator()
  3. IEnumerator IEnumerable.GetEnumerator()

当c#编译foreach循环时,它将按上述顺序选择枚举数。注意,一个类型不需要实现IEnumerableIEnumerable<T>来成为可枚举的,它只需要一个名为GetEnumerator()的方法来返回一个枚举器。

现在,List<T>.GetEnumerator()具有静态类型的优点,这使得所有对List<T>.Enumerator.get_CurrentList<T>.Enumerator.MoveNext()的调用都是静态绑定的,而不是虚拟的。

10M迭代(coreclr):

for(int i ...)               73 ms
foreach(... List<T>)        215 ms
foreach(... IEnumerable<T>) 698 ms
foreach(... IEnumerable)   1028 ms
for(int *p ...)              50 ms

10M迭代(框架):

for(int i ...)              210 ms
foreach(... List<T>)        252 ms
foreach(... IEnumerable<T>) 537 ms
foreach(... IEnumerable)    844 ms
for(int *p ...)             202 ms

免责声明

我应该指出,列表中的实际迭代很少是瓶颈。请记住,这是数百万次迭代中数百毫秒的时间。在循环中,任何比一些算术运算更复杂的工作都将比迭代本身更加昂贵。

List<T> IEnumerable<T>。当您迭代List<T>时,您执行的操作序列与任何其他IEnumerable<T>相同:

  • 获取IEnumerator<T> .
  • 在枚举器上调用IEnumerator<T>.MoveNext()
  • 从IEnumerator接口获取IEnumerator<T>.Current元素,而MoveNext()返回true
  • 处理IEnumerator<T> .

我们知道List<T>是一个内存中的集合,所以MoveNext()函数在它的枚举器上的开销将非常小。看起来你的集合给出了一个枚举器,它的MoveNext()方法更昂贵,可能是因为它与一些外部资源(如数据库连接)交互。

当你在IEnumerable<T>上调用ToList()时,你正在运行一个完整的集合迭代,并通过该迭代将所有元素加载到内存中。如果您希望多次迭代同一集合,那么这样做是值得的。如果您希望只对集合进行一次迭代,那么ToList()是一个错误的经济:它所做的只是创建一个内存中的集合,稍后必须对其进行垃圾收集。

List<T>IEnumerable<T>接口的实现。要使用foreach语法,不需要List<T>类型或IEnumerable<T>类型,但需要使用带有GetEnumerator()方法的类型。引用自Microsoft文档:

foreach语句并不局限于这些类型。您可以将它与满足以下条件的任何类型的>实例一起使用:

  • 类型具有公共无参数GetEnumerator方法,其返回类型可以是类、结构或接口类型。开始c# 9.0中,GetEnumerator方法可以是一个类型的扩展方法。
  • GetEnumerator方法的返回类型具有公共Current属性和公共无参数MoveNext方法,其返回

考虑到例如一个LINQ上下文,执行查询,使用IEnumerable结构,你有延迟执行查询的优势(查询将只在需要时执行),但是,使用ToList()方法,你要求查询必须立即执行(或求值),你希望你的结果在内存中,将它们保存在一个列表中,以便以后对它们执行一些操作,比如更改一些值。

关于性能,它取决于你想做什么。我们不知道你正在执行哪些操作(比如从数据库中获取数据),你正在使用哪些集合类型等等。

IEnumerable集合迭代和List集合迭代的时间不同的原因是,正如我所说的,当调用

时,查询的执行被延迟了:
IEnumerable<SimpleEntity> entities = from entity in Session.Query<SimpleEntity>
                                             select entity;

这意味着查询只在迭代IEnumerable集合时执行。由于上述原因,在entities.ToList();中调用ToList()方法时不会发生这种情况。

我认为这与IEnumerable无关。这是因为在第一个循环中,当你在IEnumerable上迭代时,你实际上是在执行查询。

这与第二种情况完全不同,当你在这里执行查询时:

List<SimpleEntity> lstEntities = entities.ToList();

使迭代更快,因为您实际上没有查询BD ,并且当处于循环中时,将结果转换为列表

如果你这样做:

foreach(SimpleEntity item in entities.ToList())
{
    //this for loop took less than a milisecond
    value += item.Price;
}

也许你会得到类似的表现。

您正在使用linq。

IEnumerable<SimpleEntity> entities = from entity in Session.Query<SimpleEntity>
                                         select entity;

只是声明查询。它将在foreach获得枚举数时执行。这1.5秒包括执行Session.Query<>

如果你测量行

List<SimpleEntity> lstEntities = entities.ToList();

你应该得到1.5秒或至少超过1秒。

你确定你采取的措施是正确的吗?您应该测量第二个循环,包括entities . tolist()。

干杯!

最新更新