LINQ是否缓存计算值?

时间:2012-04-25 01:56:27

标签: c# .net linq clr4.0

假设我有以下代码:

var X = XElement.Parse (@"
    <ROOT>
        <MUL v='2' />
        <MUL v='3' />
    </ROOT>
");
Enumerable.Range (1, 100)
    .Select (s => X.Elements ()
        .Select (t => Int32.Parse (t.Attribute ("v").Value))
        .Aggregate (s, (t, u) => t * u)
    )
    .ToList ()
    .ForEach (s => Console.WriteLine (s));

.NET运行时实际上在这里做什么?是解析并将属性转换为100次中的整数,还是足够聪明,以确定它应该缓存解析后的值而不重复该范围内每个元素的计算?

而且,我怎么会自己搞清楚这样的事情?

提前感谢您的帮助。

2 个答案:

答案 0 :(得分:4)

LINQ和IEnumerable<T> 基于拉。这意味着,在拉取值之前,不会执行作为LINQ语句一部分的谓词和操作。此外,每次拉取值时都会执行谓词和操作(例如,没有秘密缓存进行)。

来自IEnumerable<T>的{​​{1}}来自foreach语句,它通过调用IEnumerable<T>.GetEnumerator()并重复调用IEnumerator<T>.MoveNext()来获取值来获取枚举数,这确实是语法糖。

ToList()ToArray()ToDictionary()ToLookup()这样的LINQ运算符会包含foreach语句,因此这些方法会进行拉动。关于Aggregate()Count()First()等运营商也可以这样说。这些方法的共同之处在于它们产生的单个结果必须通过执行foreach语句来创建。

许多LINQ运算符生成一个新的IEnumerable<T>序列。当从结果序列中拉出元素时,操作员从源序列中提取一个或多个元素。 Select()运算符是最明显的示例,但其他示例包括SelectMany()Where()Concat()Union()Distinct(),{{1} }和Skip()。这些运算符不进行任何缓存。当从Take()拉出第N个元素时,它从源序列中拉出第N个元素,使用提供的动作应用投影并返回它。这里没什么秘密。

其他LINQ运算符也会生成新的Select()序列,但它们是通过实际拉动整个源序列,完成其工作然后生成新序列来实现的。这些方法包括IEnumerable<T>Reverse()OrderBy()。但是,只有当操作符本身被拉动时才会执行操作符的拉取,这意味着在执行任何操作之前,您仍然需要LINQ语句的“{1}}循环”。您可以争辩说这些运算符使用缓存,因为它们会立即拉出整个源序列。但是,每次迭代运算符时都会构建此缓存,因此它实际上是一个实现细节,而不是神奇地检测到您对同一序列多次应用相同的GroupBy()运算。


在您的示例中,foreach会进行拉动。外OrderBy()中的操作将执行100次。每次执行此操作时,ToList()将执行另一次解析XML属性的拉动。您的代码总共会调用Select 200次。

您可以通过拉一次属性而不是每次迭代来改进这一点:

Aggregate()

现在Int32.Parse()仅被调用2次。但是,成本是必须分配,存储并最终收集垃圾的属性值列表。 (当列表包含两个元素时,这不是一个大问题。)

请注意,如果您忘记了第一个提取属性的var X = XElement.Parse (@" <ROOT> <MUL v='2' /> <MUL v='3' /> </ROOT> ") .Elements () .Select (t => Int32.Parse (t.Attribute ("v").Value)) .ToList (); Enumerable.Range (1, 100) .Select (s => x.Aggregate (s, (t, u) => t * u)) .ToList () .ForEach (s => Console.WriteLine (s)); ,代码仍将运行但具有与原始代码完全相同的性能特征。没有空间用于存储属性,但是在每次迭代时都会对它们进行解析。

答案 1 :(得分:2)

我挖掘这段代码已经有一段时间但是,IIRC,Select的工作方式是简单地缓存你提供它的Func并一次一个地在源集合上运行它。因此,对于外部范围中的每个元素,它将运行内部Select/Aggregate序列,就像它是第一次一样。没有任何内置缓存 - 你必须自己在表达式中实现它。

如果你想自己解决这个问题,你有三个基本选择:

  1. 编译代码并使用ildasm查看IL;它是最准确的,但是,特别是对于lambdas和闭包,你从IL获得的东西可能看起来与你放入C#编译器的内容完全不同。
  2. 使用像dotPeek这样的东西将System.Linq.dll反编译成C#;再一次,你从这些工具中获得的东西可能只是大致类似于原始的源代码,但至少它将是C#(特别是dotPeek做得非常好,并且是免费的。)
  3. 我的个人偏好 - 下载.NET 4.0 Reference Source并自己寻找;这就是它的用途:)你必须只相信MS,参考源与用于生成二进制文件的实际来源相匹配,但我认为没有任何理由怀疑它们。
  4. 正如@AllonGuralnek所指出的,你可以在一行中的特定lambda表达式上设置断点;将光标放在lambda体内的某个位置并按F9,它将断点为lambda。 (如果你做错了,它会突出显示断点颜色的整行;如果你做得对,它只会突出显示lambda。)