迭代到IEnumerable 和List 之间的性能

今天,我在迭代一系列项目时遇到了性能问题。 在完成一些诊断后,我终于找到了降低性能的原因。 事实certificate,迭代IEnumerable花费的时间比迭代List要多得多。 请帮助我理解为什么IEnumerableList慢。

更新基准上下文:

我正在使用NHibernate从数据库中获取项目集合到IEnumerable并将其属性的值相加。 这只是一个没有任何引用类型的简单实体:

 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 entities = from entity in Session.Query select entity; decimal value = 0.0; foreach(SimpleEntity item in entities) { //this for loop took 1.5 seconds value += item.Price; } List lstEntities = entities.ToList(); foreach(SimpleEntity item in lstEntities) { //this for loop took less than a milisecond value += item.Price; } } } 

List IEnumerable 。 当您遍历List ,您正在执行与任何其他IEnumerable相同的操作序列:

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

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

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

ListIEnumerable接口的实现。 List然后允许您使用foreach语法。

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

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

UPDATE

您在IEnumerable集合迭代和List集合迭代之间有不同的时间的原因,就像我说的那样,当您调用时,您有一个延迟执行的查询:

 IEnumerable entities = from entity in Session.Query select entity; 

这意味着只有在迭代IEnumerable集合时才执行查询。 当你在entities.ToList();调用ToList()方法时,这不会发生entities.ToList(); 由于我上面描述的原因。

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

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

 List lstEntities = entities.ToList(); 

使迭代更快,因为您实际上并未查询BD 在循环中将结果转换为列表。

如果你这样做:

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

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

枚举IEnumerable比直接枚举相同的List慢2到3倍。 这是由于C#如何为给定类型选择它的枚举器的细微之处。

List公开3个枚举器:

  1. List.Enumerator List.GetEnumerator()
  2. IEnumerator IEnumerable.GetEnumerator()
  3. IEnumerator IEnumerable.GetEnumerator()

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

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

10M次迭代(coreclr):

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

10M迭代(框架):

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

放弃

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