在哪些情况下IEnumerable <t> .Count优化?</t>

时间:2010-02-01 23:19:38

标签: c# optimization count ienumerable

使用reflector我注意到System.Linq.Enumerable.Count方法有条件优化它,以便在IEnumerable<T>传递的情况下实际上是ICollection<T>。如果转换成功,则Count方法不需要迭代每个元素,但可以调用ICollection的Count方法。

基于此,我开始认为IEnumerable<T>可以像集合的只读视图一样使用,而不会出现我最初期望的基于IEnumerable<T>的API的性能损失

Count是对IEnumerable<T>语句的Select语句的结果,但基于反映的代码时,我对ICollection的优化是否仍然有效感兴趣case未经优化,需要迭代所有元素。

你从反射器得出相同的结论吗?缺少这种优化背后的原因是什么?我似乎在这个常见的操作中浪费了很多时间。规范是否需要即使可以在不这样做的情况下确定Count,也会评估每个元素?

3 个答案:

答案 0 :(得分:12)

Select的结果被懒惰地评估并不重要。 Count始终等同于原始集合的计数,因此可以通过返回Select中可用于短路评估Count的特定对象来直接检索它。方法。

无法根据具有确定数量的内容(如Count())优化对Select调用返回值的List<T>方法评估的原因是它可以改变程序的含义。

传递给selector方法的Select函数允许具有副作用,并且必须以预定的顺序确定性地发生其副作用。

假设:

new[]{1,2,3}.Select(i => { Console.WriteLine(i); return 0; }).Count();

文档要求此代码打印

  

1
  2
  3

即使从开始开始知道计数并且可以优化,优化也会改变程序的行为。这就是为什么你无论如何都无法避免集合的枚举。这正是编译器优化在纯函数式语言中更容易的原因之一。


更新:显然,实施SelectCount是完全可能的,这样Select上的ICollection<T>仍然可以懒惰地评估,但Count()将在O(1)中进行评估而不枚举集合。我将在不改变任何方法的界面的情况下这样做。类似的事情已经为ICollection<T>完成了:

private interface IDirectlyCountable {
    int Count {get;}
}
private class SelectICollectionIterator<TSource,TResult> : IEnumerable<T>, IDirectlyCountable {
     ICollection<TSource> sequence;
     Func<TSource,TResult> selector;
     public SelectICollectionIterator(ICollection<TSource> source, Func<TSource,TResult> selector) {
         this.sequence = source;
         this.selector = selector;
     }
     public int Count { get { return sequence.Count; } }
     // ... GetEnumerator ... 
}
public static IEnumerable<TResult> Select<TSource,TResult>(this IEnumerable<TSource> source, Func<TSource,TResult> selector) {
    // ... error handling omitted for brevity ...
    if (source is ICollection<TSource>)
       return new SelectICollectionIterator<TSource,TResult>((ICollection<TSource>)source, selector);
    // ... rest of the method ...
}
public static int Count<T>(this IEnumerable<T> source) {
    // ...
    ICollection<T> collection = source as ICollection<T>;
    if (collection != null) return collection.Count;
    IDirectlyCountable countableSequence = source as IDirectlyCountable;
    if (countableSequence != null) return countableSequence.Count;
    // ... enumerate and count the sequence ...
}

这仍然会懒惰地评估Count。如果更改基础集合,则计数将更改,并且不会缓存序列。唯一的区别是不会在selector委托中执行副作用。

答案 1 :(得分:1)

编辑02-Feb-2010

正如我所看到的,至少有两种方法可以解释这个问题。

  

为什么Select<T, TResult>扩展方法何时   调用了一个类的实例   实现ICollection<T>,而不是   返回提供a的对象   Count财产;为什么   Count<T>扩展方法不是   检查这个属性,以便   当两者提供O(1)表现时   方法是链接的?

此版本的问题不会对Linq扩展如何工作做出错误的假设,并且是一个有效的问题,因为调用ICollection<T>.Select.Count毕竟总是会返回与ICollection<T>.Count相同的值。这就是迈赫达德解释这个问题的方式,他对此提出了彻底的回应。

将问题视为要求......

  

如果Count<T>扩展方法提供O(1)   一个类的对象的性能   实施ICollection<T>,为什么   它是否提供O(n)性能   的回报值   Select<T, TResult>   扩展方法?

在这个版本的问题中,有一个错误的假设:Linq扩展方法通过一个接一个地组装小集合(在内存中)并通过IEnumerable<T>接口公开它们来协同工作。

如果这是Linq扩展的工作方式,那么Select方法可能如下所示:

public static IEnumerable<TResult> Select<T, TResult>(this IEnumerable<T> source, Func<T, TResult> selector) {
    List<TResult> results = new List<TResult>();

    foreach (T input in source)
        results.Add(selector(input));

    return results;
}

此外,如果 Select的实现,我认为您会发现大多数使用此方法的代码行为都相同。但这会很浪费,事实上会在某些情况下引起例外情况,例如我在原始答案中所描述的情况。

实际上,我认为Select方法的实现更接近这样的事情:

public static IEnumerable<TResult> Select<T, TResult>(this IEnumerable<T> source, Func<T, TResult> selector) {
    foreach (T input in source)
        yield return selector(input);

    yield break;
}

这是为了提供延迟评估,并解释为什么在{(1)}方法的O(1)时间内无法访问Count属性。

换句话说,虽然Mehrdad回答了为什么Count不是设计的问题,但Select表现不同,我已经提出了我对为什么Select.Count行为方式的问题的最佳答案。


原始回答

方法副作用不是答案。

根据Mehrdad的回答:

  

这并不重要   Select的结果是懒惰的评估。

我不买这个。让我解释一下原因。

对于初学者,请考虑以下两种非常相似的方法:

Select.Count

好的,这些方法有什么作用?每个人返回与用户期望的一样多的随机双打(最多public static IEnumerable<double> GetRandomsAsEnumerable(int N) { Random r = new Random(); for (int i = 0; i < N; ++i) yield return r.NextDouble(); yield break; } public static double[] GetRandomsAsArray(int N) { Random r = new Random(); double[] values = new double[N]; for (int i = 0; i < N; ++i) values[i] = r.NextDouble(); return values; } )。这两种方法是否被懒惰评估是否重要?要回答这个问题,我们来看看下面的代码:

int.MaxValue

你能猜出这两个方法调用会发生什么吗?让我免去你复制这段代码并自己测试的麻烦:

第一个变量public static double Invert(double value) { return 1.0 / value; } public static void Test() { int a = GetRandomsAsEnumerable(int.MaxValue).Select(Invert).Count(); int b = GetRandomsAsArray(int.MaxValue).Select(Invert).Count(); } 将(在可能的大量时间之后)初始化为a(目前为2147483647)。 第二个一个int.MaxValue很可能会被b中断。

因为OutOfMemoryException和其他Linq扩展方法被懒惰地评估,它们允许你做你根本做不到的事情。以上是一个相当简单的例子。但我的主要观点是对懒惰评估并不重要的断言提出异议。 Mehrdad声明Select属性“从一开始就是真正知道并且可以被优化”实际上引发了一个问题。对于Count方法,问题似乎很简单,但Select并不是特别的;它返回一个Select,就像Linq扩展方法的其余部分一样,并且这些方法“知道”其返回值的IEnumerable<T> 将需要缓存完整的集合,因此禁止延迟评价

懒惰的评价就是答案。

出于这个原因,我必须同意其中一个原始响应者(现在答案似乎已经消失),懒惰评估确实是这里的答案。需要考虑方法副作用的想法实际上是次要的,因为无论如何这已经被确定为懒惰评估的副产品。

后记:我发表了非常自信的陈述并强调了我的观点,主要是因为我想明确我的论点是什么,而不是出于对任何其他回应的不尊重,包括Mehrdad,我认为这是有洞察力的但是错过了标记。

答案 2 :(得分:0)

ICollection知道它包含的项目数(Count)。它不必迭代任何项来确定它。以HashSet类(实现ICollection)为例。

IEnumerable<T>不知道它包含多少项。您必须枚举整个列表以确定项目数(计数)。

在LINQ语句中包装ICollection并不会提高效率。无论你怎么扭转转弯,都必须列举ICollection。