迭代到IEnumerable 和List 之间的性能
今天,我在迭代一系列项目时遇到了性能问题。 在完成一些诊断后,我终于找到了降低性能的原因。 事实certificate,迭代IEnumerable
花费的时间比迭代List
要多得多。 请帮助我理解为什么IEnumerable
比List
慢。
更新基准上下文:
我正在使用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()
是一个虚假的经济:它所做的只是创建一个内存集合,以后必须进行垃圾收集。
List
是IEnumerable
接口的实现。 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个枚举器:
-
List
.Enumerator List .GetEnumerator() -
IEnumerator
IEnumerable .GetEnumerator() -
IEnumerator IEnumerable.GetEnumerator()
当C#编译foreach
循环时,它将按上述顺序选择枚举器。 请注意,类型不需要实现IEnumerable
或IEnumerable
是可枚举的,它只需要一个名为GetEnumerator()
的方法来返回枚举器。
现在, List
具有静态类型的优点,它使得对List
和List
所有调用都是静态绑定而不是虚拟。
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
放弃
我应该指出列表中的实际迭代很少是瓶颈。 请记住,数百万次迭代的数百毫秒。 循环中的任何工作比一些算术运算更复杂,这将比迭代本身花费更多。