迭代到IEnumerable <T>和List <T>之间的性能

Doa*_*ong 7 c# ienumerable list

今天,我在迭代一系列项目时遇到了性能问题.在完成一些诊断之后,我终于找到了降低性能的原因.事实证明,迭代IEnumerable<T>一次比花费更多的时间List<T>.请帮我理解为什么IEnumerable<T>比慢List<T>.

UPDATE基准上下文:

我正在使用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;
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

Rob*_*don 8

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

  • 得到一个IEnumerator<T>.
  • 调用IEnumerator<T>.MoveNext()您的枚举器.
  • 返回时IEnumerator<T>.Current从IEnumerator接口获取元素.MoveNext()true
  • 处置IEnumerator<T>.

我们所知道的List<T>是它是一个内存中的集合,因此MoveNext()它的枚举器上的函数将非常便宜.看起来您的集合提供了一个枚举器,其MoveNext()方法更昂贵,可能是因为它与某些外部资源(如数据库连接)进行交互.

当您调用ToList()IEnumerable<T>,您正在运行集合的完整迭代,并使用该迭代将所有元素加载到内存中.如果您希望多次迭代同一个集合,这是值得的.如果您希望只迭代集合一次,那么这ToList()是一种虚假的经济:它所做的只是创建一个内存集合,以后必须进行垃圾收集.


Coi*_*oin 8

枚举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
Run Code Online (Sandbox Code Playgroud)

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
Run Code Online (Sandbox Code Playgroud)

免责声明

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

  • 很好的答案。然而,你的免责声明遗漏了一个非常重要的一点。如果您在**非常重的**内存压力下进行迭代,算术运算不一定比迭代成本高得多。在这种情况下,枚举器的分配实际上变得非常重要,因为它给 GC 带来了额外的压力。 (2认同)