【问题标题】:Caching when using Query Expressions?使用查询表达式时缓存?
【发布时间】:2010-11-09 05:22:30
【问题描述】:

我正在阅读一篇关于查询表达式如何延迟执行的文章。这是否意味着当我们有这样的集合时:

IEnumerable<int> collection = from i in integers where i % 2 == 0 select i;

每次访问collection都会重新计算?

如果是这样,处理此问题的一般做法是什么?转换成新系列?

还有为什么 C# 设计者会选择这种方式,而不是在第一次访问集合后将结果缓存到集合中?

运行时如何知道collection 的行为方式(延迟执行),不像另一个IEnumerable&lt;T&gt; 我可能使用不延迟执行的List&lt;T&gt; 创建?


编辑:

这样的情况呢:

List<int> ints = new List<int> ( ) { 1, 2, 3, 4, 5, 6, 7, 8, 9, 0 };

var even = from i in ints where i % 2 == 0 select i;

ints.AddRange ( new List<int> ( ) { 10, 20, 30, 40, 50 } );

foreach ( int i in even )
{
    Console.WriteLine ( i );
}

输出:

2, 4, 6, 8, 0, 10, 20, 30, 40, 50

通过缓存,行为是否更符合预期?

【问题讨论】:

  • 您的想法完全错误。 “偶数”根本不是一个集合。这是一个查询。它是一个代表查询“过滤整数以便您只获得偶数”的对象。就是这样,只是一个代表该查询的对象。无论何时枚举它,查询都会执行。当您这样考虑时,就会清楚为什么没有缓存任何内容:查询结果可能每次都不同,但查询并没有改变。
  • 谢谢埃里克。我现在明白了。我没有这样想。

标签: c# .net linq performance collections


【解决方案1】:

是的,每次都会重新计算。

如果您想缓存它,请在结果上使用ToArray()(如果您想稍后添加新项目,请使用ToList())。

第一次尝试后它不缓存它的原因是因为以线程安全的方式正确执行它是相当棘手的。通常,枚举集合被认为是线程安全的操作。但是,如果实现会在枚举项目时缓存它们,那么它对于多个读取器来说要么变得不安全(令用户感到惊讶并且非常不受欢迎),要么在每一步都对缓存进行显式锁定,这会显着影响性能。即使您实际上没有多个线程同时读取集合,这种性能损失也会存在 - 锁本质上是昂贵的。

另一点是,当查询表达式引用在执行点和枚举结果点之间发生变化的可变变量时,缓存行为不是很明显。当您进行急切评估时,这些值就是它们在查询点的值。当您执行非缓存延迟评估时,值始终是它们在枚举点的值。使用缓存,您不知道 - 这取决于您是否是第一次枚举它。

还有一点是它无法真正知道是否需要缓存。如果您只打算枚举序列一次(这是一种非常常见的情况),那么缓存项目会浪费时间和内存。

F# 实际上有一个用于这种惰性缓存序列的标准类型,称为 LazyList - 您可以将任何 IEnumerable 包装到其中以获得所需的语义,并注意上述所有注意事项。

【讨论】:

  • 谢谢帕维尔。在我的编辑中,这就是你的第 4 段的意思吗?
  • 我不确定。查看您的编辑,似乎“缓存”实际上只是指“在枚举开始时缓存值”,而是“在执行查询时完全执行查询并存储结果”。你在我的回复中提到的那段是前一种情况,而不是后一种情况。
【解决方案2】:

我发现将IEnumerable&lt;T&gt; 视为一个序列而不是一个集合更容易,这是 F# 使用的术语。从根本上说,IEnumerable 的所有承诺是它可以返回一个IEnumerator,它本身提供了一个简单的合约,它恰好有一个花哨的语言结构以使其易于使用:foreach

因此,与其将 LINQ 视为过滤您的集合,不如将方法视为在枚举时满足您的条件的返回序列。当你的查询被编译成这个...

IEnumerable<int> collection = integers.Where(i => i % 2 == 0);

...将collection 视为integers 中偶数值的序列,而不是此类整数的“集合”。我什至会将collection 重命名为更准确的名称,例如evenIntegers

回答您的具体问题:

  1. 每次访问集合时都会重新计算?

    每次调用collection.GetEnumerator() 都会返回一个新的枚举器,是的。事实上,如果你枚举collection,然后更新integers,再次枚举collection会产生不同的结果。调用额外的延迟 LINQ 运算符只会返回对过滤后的 collection 序列进行操作的新序列,在枚举它们之前也不会实际计算任何内容。

  2. 如果是这样,处理此问题的一般做法是什么?转换成新系列?

    一般来说,您应该尽可能地推迟执行。通过链接各种 LINQ 方法,可以构建一个真正“智能”的序列,在使用 foreach(或 FirstLastCount 等聚合)之前实际上不会做任何事情。

  3. 还有为什么 C# 设计者会选择这种方式,而不是在第一次访问集合后将结果缓存到集合中?

    提供了各种直接运算符(ToArray、ToList、ToDictionary、ToLookup)来支持需要内存中集合的场景。缓存延迟序列很容易;无法取消延迟缓存的序列。

  4. 运行时如何知道集合以这种方式运行(延迟执行),这与我可能使用不延迟执行的 List 创建的另一个 IEnumerable 不同?

    正如我之前所建议的,运行时并不真正了解特定 IEnumerable&lt;T&gt; 的行为方式。它只知道它将提供一个枚举器。由个人实现者(如List&lt;T&gt;)决定它想要的行为方式。

【讨论】:

  • 有关不是“真实”集合的序列的有趣示例,请查看 Continuous LINQ:codeplex.com/clinq
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多