使用查询表达式时缓存?

时间:2009-07-08 23:40:51

标签: c# .net linq performance collections

我正在阅读一篇关于查询表达式如何推迟执行的文章。这是否意味着我们有一个像:

这样的集合
IEnumerable<int> collection = from i in integers where i % 2 == 0 select i;

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

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

为什么C#设计师选择这种方式,而不是在第一次访问该系列后将结果缓存到集合中的东西?

运行时如何知道collection以这种方式行事(延迟执行),不像我可能使用不会延迟执行的IEnumerable<T>创建的另一个List<T>


编辑:

这样的案例怎么样:

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

通过缓存,行为会更加期待吗?

2 个答案:

答案 0 :(得分:2)

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

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

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

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

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

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

答案 1 :(得分:2)

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

因此,不要将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(或FirstLast,像{Count这样的聚合,否则它实际上不会做任何事情。 {1}}等。)

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

    提供各种直接运算符(ToArray,ToList,ToDictionary,ToLookup)以支持需要内存中集合的方案。缓存延迟序列很容易;

  4. 是不可能取消延迟缓存的序列
  5. 此外,运行时如何知道集合的行为方式(延迟执行),不像我可能使用不会延迟执行的List创建的另一个IEnumerable?

    正如我之前建议的那样,运行时并不真正了解特定IEnumerable<T>的行为方式。它只知道它将提供一个枚举器。由个人实施者(如List<T>)来决定它的行为方式。