今天,我在迭代一系列项目时遇到了性能问题。在完成一些诊断之后,我终于找到了降低性能的原因。事实证明,迭代IEnumerable<T>
花费的时间比迭代List<T>
要多得多。请帮助我理解为什么IEnumerable<T>
比List<T>
慢。
更新基准上下文:
我使用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;
}
}
}
答案 0 :(得分:7)
枚举IEnumerable<T>
的速度比直接枚举同一List<T>
的速度慢2至3倍。这是由于C#如何为给定类型选择其枚举数。
List<T>
公开了3个枚举器:
List<T>.Enumerator List<T>.GetEnumerator()
IEnumerator<T> IEnumerable<T>.GetEnumerator()
IEnumerator IEnumerable.GetEnumerator()
当C#编译foreach
循环时,它将按上述顺序选择枚举数。请注意,一种类型无需实现IEnumerable
或IEnumerable<T>
即可枚举,它只需要一个名为GetEnumerator()
的方法即可返回枚举器。
现在,List<T>.GetEnumerator()
具有静态类型化的优点,这使得对List<T>.Enumerator.get_Current
和List<T>.Enumerator.MoveNext()
的所有调用都是静态绑定的,而不是虚拟的。
1000万次迭代(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
1000万次迭代(框架):
for(int i ...) 210 ms
foreach(... List<T>) 252 ms
foreach(... IEnumerable<T>) 537 ms
foreach(... IEnumerable) 844 ms
for(int *p ...) 202 ms
免责声明
我应该指出,列表中的实际迭代很少是瓶颈。请记住,这是数百万次迭代中的数百毫秒。循环中比一些算术运算更复杂的任何工作都将比迭代本身成本高得多。
答案 1 :(得分:5)
List<T>
IEnumerable<T>
。当您遍历List<T>
时,您正在执行与其他任何IEnumerable<T>
相同的操作序列:
IEnumerator<T>
。IEnumerator<T>.MoveNext()
。IEnumerator<T>.Current
返回MoveNext()
时,从IEnumerator界面获取true
元素。IEnumerator<T>
。我们对List<T>
的了解是它是一个内存中的集合,因此其枚举器上的MoveNext()
函数将非常便宜。看起来您的集合提供了一个枚举器,其MoveNext()
方法更昂贵,可能是因为它与某些外部资源(如数据库连接)进行交互。
当您在ToList()
上调用IEnumerable<T>
时,您正在运行集合的完整迭代,并使用该迭代将所有元素加载到内存中。如果您希望多次迭代同一个集合,这是值得的。如果您希望只遍历集合一次,那么ToList()
是一个虚假的经济:它所做的只是创建一个内存集合,以后必须进行垃圾收集。
答案 2 :(得分:4)
List<T>
是IEnumerable<T>
接口的实现。然后List<T>
允许您使用foreach
语法。
考虑例如LINQ上下文,执行查询,使用IEnumerable
结构,您有延迟执行查询的优势(查询将仅在需要时执行),但是,使用{{ 1}}方法,您要求立即执行(或评估)查询,并且您希望将结果保存在内存中,将它们保存在列表中,以便稍后对它们执行某些操作,例如更改某些值。 / p>
关于效果,取决于您正在尝试做什么。我们不知道您正在执行哪些操作(例如从数据库中获取数据),您正在使用的集合类型等等。
<强>更新强>
在IEnumerable集合迭代和List集合迭代之间有不同的时序的原因,就像我说的那样,当你调用时你有一个延迟执行的查询:
ToList()
这意味着只有在您对IEnumerable集合进行迭代时才会执行查询。由于上述原因,当您在IEnumerable<SimpleEntity> entities = from entity in Session.Query<SimpleEntity>
select entity;
中调用ToList()
方法时,这种情况不会发生。
答案 3 :(得分:3)
我认为它与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;
}
也许你会得到类似的表现。