我已经看到,如果LINQ to Objects查询被迫使用.ToArray()
强制立即执行,有时可以显着提高它们的性能,但是不能理解为什么。例如,在下面的示例中,函数Deferred()
的执行比函数Immediate()
慢得多,并且以limit
的值呈指数增长(也许它在两个函数中都呈指数关系) ,但Immediate()
的执行时间太少,我无法直接说出来。
public void Deferred()
{
var all = Range(limit);
var even = from e in EvenRange(limit) where all.Contains(e) select e;
var odd = from o in OddRange(limit) where !even.Contains(o) select o;
var query = from q in odd select q;
foreach(var i in query) { var j = i+1; }
}
public void Immediate()
{
var all = Range(limit);
var even = (from e in EvenRange(limit) where all.Contains(e) select e)
.ToArray();
var odd = (from o in OddRange(limit) where !even.Contains(o) select o)
.ToArray();
var query = (from q in odd select q).ToArray();
foreach(var i in query) { var j = i+1; }
}
public static IEnumerable<int> OddRange(int stop)
{
for (int i = 1; i < stop; i+=2) yield return i;
}
public static IEnumerable<int> EvenRange(int stop)
{
for (int i = 2; i < stop; i+=2) yield return i;
}
public static IEnumerable<int> Range(int stop)
{
for (int i = 0; i < stop; ++i) yield return i;
}
导致这种放缓的原因是什么?它只是编译器生成的额外代码,每次访问可枚举时都必须运行,或者Deferred()
是否也会进入一些额外的循环,从而改变查询的“大O”复杂性?
答案 0 :(得分:6)
据我所知,IEnumerable不会缓存结果,所以你的子句涉及even.Contains(必须检查所有偶数元素)强制完全重新枚举even,从而显示你注意到的性能行为。即使在数组或列表中进行转换也应该在枚举奇数时显示出良好的性能。其他.NET语言(如F#)包含缓存枚举结果的方法(如果您有兴趣,可以查看F#的Seq.cache方法)
答案 1 :(得分:2)
正如emaster70所解释的那样,Deferred
版本会在每次调用even
时重新计算even.Contains
个集合。这使得它在limit
的更高值时会非常快地降级。
Immediate
版本更好,如果您也立即制作all
集合,请执行以下操作:
var all = Range(limit).ToArray();
它的速度提高了大约三倍。
但是,数组的Contains
方法是O(n)操作,因此也不能很好地扩展。如果条件相等,则应使用连接而不是使用Contains
查找每个值。
如果需要进行查找,则应使用HashSet
而不是数组,因为Contains
方法是O(1)操作。
对于10000的limit
,这比Immediate
版本快100倍:
public void Joined() {
var all = Range(limit);
var even = from e in EvenRange(limit) join a in all on e equals a select e;
var evenSet = new HashSet<int>(even);
var odd = from o in OddRange(limit) where !evenSet.Contains(o) select o;
var query = from q in odd select q;
foreach (var i in query) { var j = i + 1; }
}
答案 2 :(得分:1)
除了emaster70所说的,当从数组中读取值时,整个缓存行被提取到系统缓存中,这意味着当查看数组时,它比你必须从主内存中获取要快得多。
答案 3 :(得分:1)
在此代码中
var all = Range(limit);
var even = from e in EvenRange(limit) where all.Contains(e) select e;
var odd = from o in OddRange(limit) where !even.Contains(o) select o;
因为从迭代器块或LINQ查询构造的IEnumerables是惰性的,所以计算'odd'的每个元素需要重新计算整个'even'序列。结果,这段代码的大O很糟糕。使用非常小的“限制”并将一个Console.WriteLine()置于Range()的循环中并观察行为可能是有益的。