今天,我在迭代条目列表时遇到了性能问题。在做了一些诊断之后,我终于找到了导致性能下降的原因。结果表明,迭代一个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个枚举数:
-
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()
的调用都是静态绑定的,而不是虚拟的。
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()。
干杯!