C# 为什么“.Select(…).Last()”是优化的,而“.Select(…).Last(…)”不是优化的?
考虑以下枚举数:C# 为什么“.Select(…).Last()”是优化的,而“.Select(…).Last(…)”不是优化的?,c#,.net,linq,C#,.net,Linq,考虑以下枚举数: var items = (new int[] { 1, 2, 3, 4, 5 }).Select(x => { Console.WriteLine($"inspect {x}"); return x; }); 这将生成元素[1,2,3,4,5],并在消费时打印它们 当我在此枚举器上调用Last方法时,它会触发一个仅访问单个元素的快速路径: items.Last(); 但是,当我将回调传递给Last时,它从一开始就遍历整个列表: items.Last(x
var items = (new int[] { 1, 2, 3, 4, 5 }).Select(x =>
{
Console.WriteLine($"inspect {x}");
return x;
});
这将生成元素[1,2,3,4,5]
,并在消费时打印它们
当我在此枚举器上调用Last
方法时,它会触发一个仅访问单个元素的快速路径:
items.Last();
但是,当我将回调传递给Last时,它从一开始就遍历整个列表:
items.Last(x => true);
通过查看.NET核心源代码,我发现:
- )李>
- )李>
- 因为,这条快速路径被触发了,一切都很好
- 这有着快速的发展道路
- 因此,它采用缓慢的路径,从一开始就迭代
IPartition
上的一个附加方法
缺乏优化也令人惊讶。由于这些重载共享相同的名称,因此可以假定它们也以类似的方式进行了优化。(至少我是这么想的。)
考虑到优化此案例的这些原因,为什么LINQ的作者选择不这样做?Last()
始终可以针对允许在恒定时间内访问集合最后一个元素的集合进行优化(O(1)
)。对于这些集合,您可以直接访问最后一个元素,而不是迭代所有集合并返回最后一个元素
从概念上讲,如果至少有一个元素满足谓词(在
练习),然后向后迭代可能允许提前退出循环
对于
Last(Func)
的一般实现来说,这种假设太牵强了。一般来说,不能假设满足谓词的最后一个元素更接近集合的末尾。该优化对您的示例非常有效(Last(x=>true)
),但对于每个这样的示例,都可能有一个优化性能更差的实例(Last(x=>false)
) 也许你应该在github上的一篇帖子中提到这一点,我不确定我们除了同意或者说hmm@TheGeneral:嗯,我们可以指出推理中的缺陷(尽管我没有发现任何缺陷),而且众所周知,一些.NET语言设计师确实阅读并回答了有关StackOverflow的问题。让我引用Stephen Toub的话,谁在另一个优化建议上这么说的:“对于任何查询处理系统,都可能会有无限多的优化,这些优化可以引入这种或那种特殊的模式。每一种优化都会增加代码和复杂性。我们的目标应该是根据实际用例优化重要的优化,因为所有这些优化都会增加其他每次使用的成本,即使只是增加了二进制大小、增加了JIT时间等,加上与额外特殊代码相关的持续维护成本。“这种推理显然没有什么错。在github上打开一个线程,看看那些酷孩子说什么,你永远不知道在未来的版本中你可能会得到一个.Net核心优化,你可以把它归因于你自己。让我们知道你的进展情况。。对于Stehpen Toub的评论,也是+1@sloth@LambdaFairy-但是您发现的OrderedEnumerable
和IList
的现有优化已经打破了它的“契约”,在列表中对所有元素的谓词进行评估。因此,任何依赖于(例如)变异谓词的人都已经在摇摇欲坠的基础上工作。“你不能假设满足谓词的最后一个元素通常更接近集合的末尾”,这不是真正的重点。查找向前移动的最后一个元素意味着您必须查看所有元素anyway@Muscicapa您的答案与框架本身相矛盾,如果枚举对象是IList
,框架本身会向后循环。
items.Last(x => true);
inspect 1
inspect 2
inspect 3
inspect 4
inspect 5