Skip的性能(以及类似功能,如Take)

Bid*_*dou 21 c# linq performance ienumerable skip-take

我刚看了.NET Framework 的Skip/ Takeextension方法的源代码(在IEnumerable<T>类型上),发现内部实现正在使用该GetEnumerator方法:

// .NET framework
    public static IEnumerable<TSource> Skip<TSource>(this IEnumerable<TSource> source, int count)  
    {
        if (source == null) throw Error.ArgumentNull("source"); 
        return SkipIterator<TSource>(source, count); 
    }

    static IEnumerable<TSource> SkipIterator<TSource>(IEnumerable<TSource> source, int count) 
    {
        using (IEnumerator<TSource> e = source.GetEnumerator()) 
        {
            while (count > 0 && e.MoveNext()) count--;
            if (count <= 0) 
            { 
                while (e.MoveNext()) yield return e.Current;
            } 
        } 
    }
Run Code Online (Sandbox Code Playgroud)

假设我有IEnumerable<T>1000个元素(底层类型是List<T>).如果我正在做list.Skip(990)会怎么样.拿(10)?在进入最后十个元素之前,它是否会通过990首元素进行迭代?(这就是我的理解).如果是,那么我不明白为什么微软没有实现这样的Skip方法:

    // Not tested... just to show the idea
    public static IEnumerable<T> Skip<T>(this IEnumerable<T> source, int count)
    {
        if (source is IList<T>)
        {
            IList<T> list = (IList<T>)source;
            for (int i = count; i < list.Count; i++)
            {
                yield return list[i];
            }
        }
        else if (source is IList)
        {
            IList list = (IList)source;
            for (int i = count; i < list.Count; i++)
            {
                yield return (T)list[i];
            }
        }
        else
        {
            // .NET framework
            using (IEnumerator<T> e = source.GetEnumerator())
            {
                while (count > 0 && e.MoveNext()) count--;
                if (count <= 0)
                {
                    while (e.MoveNext()) yield return e.Current;
                }
            }
        }
    }
Run Code Online (Sandbox Code Playgroud)

事实上,他们为Count方法做了这样的例子......

    // .NET Framework...
    public static int Count<TSource>(this IEnumerable<TSource> source) 
    {
        if (source == null) throw Error.ArgumentNull("source");

        ICollection<TSource> collectionoft = source as ICollection<TSource>; 
        if (collectionoft != null) return collectionoft.Count;

        ICollection collection = source as ICollection; 
        if (collection != null) return collection.Count; 

        int count = 0;
        using (IEnumerator<TSource> e = source.GetEnumerator())
        { 
            checked 
            {
                while (e.MoveNext()) count++;
            }
        } 
        return count;
    } 
Run Code Online (Sandbox Code Playgroud)

那是什么原因?

Sve*_*sen 14

在Jon Skeet重新实施Linq的优秀教程中,他(简要地)讨论了这个问题:

虽然大多数这些操作都无法进行合理优化,但在源实现IList时优化Skip是有意义的.我们可以跳过跳过,可以这么说,直接找到适当的索引.这不会发现在迭代之间修改源的情况,这可能是我所知道的在框架中没有实现的一个原因.

这似乎是推迟优化的合理理由,但我同意,对于特定情况,如果您可以保证您的源不能/不会被修改,那么进行优化可能是值得的.

  • 出于功能目的,IList基本上是一个缓存集合.在枚举器迭代之间修改IList通常会引发异常,所以我不明白为什么不应该进行优化.事实上,如果您接受随机平行副作用修改列表的可能性,无论您是否直接跳过,结果都将是随机的. (2认同)
  • 我有一项任务来优化一些需要数小时/数天才能完成执行的代码。当我运行 Visual Studio Profiler 时,我发现很少有地方可以优化,但 Profiler 没有显示 Skip/Take 性能问题。当我切换到索引并删除 Skip/Take 时,我将性能提高了 1700 倍:代码执行了大约 9.5 小时,现在它只工作了 20 秒,所以如果你需要良好的性能,不要使用 Skip/Take for IList。 (2认同)