【问题标题】:LINQ/IEnumerable Skip().Take() efficiency with used with "yield return"LINQ/IEnumerable Skip().Take() 效率与“收益回报”一起使用
【发布时间】:2014-02-19 13:41:19
【问题描述】:

我对@9​​87654322@ 和Take()IEnumerable<> 一起使用时的效率有疑问。

我使用IEnumerable<> 返回我的所有数据列表,并且我使用“yield return”来防止我必须分配大量内存来传回数据。这非常有效。

但是,在我的流程后期,我想批量处理这些数据,并一次从我的列表中提取 20 个条目。我心想..啊!这非常适合枚举器。

我在IEnumerable interface 上发现了非常有用的Skip()Take() 方法,但是我现在意识到这会导致我的循环每次从头开始重新交互。

IEnumerable 分页数据的最佳方式是什么?我最好在枚举器上使用MoveFirst()MoveNext() 而不是Skip()Take()

我已经做了一些谷歌搜索,但找不到答案..

有人可以帮忙吗?

我真的很喜欢 IEnumerable<> 上的 LINQ 功能,但是我真的必须考虑效率。

【问题讨论】:

  • 查看“更多 Linq”,特别是 Batch method
  • 有很多方法可以实现分页。最佳答案很大程度上取决于您的环境和背景。

标签: c# performance linq enumerable


【解决方案1】:

您可以编写一个Batch 方法将一系列项目转换为给定大小的批次序列,这可以在不需要多次迭代源序列的情况下完成,并且可以将内存占用限制为仅一次在内存中保存一批的大小:

public static IEnumerable<IEnumerable<T>> Batch<T>(
    this IEnumerable<T> source, int batchSize)
{
    List<T> buffer = new List<T>(batchSize);

    foreach (T item in source)
    {
        buffer.Add(item);

        if (buffer.Count >= batchSize)
        {
            yield return buffer;
            buffer = new List<T>(batchSize);
        }
    }
    if (buffer.Count > 0)
    {
        yield return buffer;
    }
}

【讨论】:

  • 在分页的上下文中,这似乎是这个问题的含义:做source.Batch(pageSize).Skip(pageNo).Take(1)source.Skip(pageNo*pageSize).Take(pageSize).ToList() 几乎相同,除了前者对跳过的页面做了更多不必要的分配。
  • @MartinLiversage OP 在哪里声明他一次只想取出一页?这里的重点是,您不会只是拉出一页,然后返回,再次批处理项目,然后再拉出另一页。你会有类似foreach(var batch in data.Batch(batchSize))processBatch(batch);
  • 我在 IEnumerable 接口上发现了非常有用的 Skip() 和 Take() 方法,但是我现在意识到这会导致我的循环每次从头开始重新交互。 向我表明他正在做source.Skip(n).Take(m) 并且发现每次他这样做时迭代都从第一个元素开始。我的观点是,要么您必须重新开始,要么您必须存储“跳过”的项目,以便下次在源上调用 Skip 时可以重用它们。没有既不消耗 CPU 也不消耗内存的神奇解决方案。
  • @MartinLiversage 但是有。如果他正在创建一个看起来像这样的循环:for(int i = 0; i &lt; numBatches; i++) processBatch(data.Skip(batchSize*i).Take(batchSize));,这听起来像是他正在做的事情,那么他可以用我的代码替换它,只迭代源序列一次,内存中永远不会超过一个批次,并且没有任何重要的不必要的 CPU 工作。这是该问题的适当解决方案。
【解决方案2】:

在内存和 CPU 之间总是需要权衡取舍。目前,您通过使用Skip 向前移动直到页面开始来获取页面的项目,并且这些项目将由迭代器块在每个页面请求上重新计算。

但是,您可以通过缓存到目前为止计算的项目来避免重新计算,但这会占用一些内存。您说您决定使用迭代器块来避免使用过多内存,但也许只缓存必要项目的“智能”解决方案可能有用?

在 Stack Overflow 问题Is there an IEnumerable implementation that only iterates over it's source (e.g. LINQ) once 的答案中,您会发现一些解决方案仅计算和存储足够的元素以移动到您的页面。例如。如果您的页面大小为 10 并且您想要第 5 页,您将只计算和存储前 60 个项目。随后对第 3 页的请求将使用已计算的项目,而对第 10 页的请求将计算并缓存足够的项目以获取该页面的数据。

如果您想执行分页而不从第一个元素开始,也不需要存储未使用的项目,您需要某种方法在特定页面重新开始迭代,而不必迭代所有先前的元素。 IEnumerable&lt;T&gt;IEnumerator&lt;T&gt; 没有提供足够的功能来做到这一点。

【讨论】:

  • 正如他在问题中所说,这里使用迭代器块的目的是减少内存占用,因为一次将整个数据集存储在内存中太多了。因此,将整个数据集缓存在内存中就无法实现这一目标。
  • @Servy:链接问题的答案(您自己提供的)不会缓存整个序列,而只会缓存生成的最小数量的项目以移动到特定页面。这解决了问题中所述的问题:_我现在意识到这会导致我的循环每次都从头开始重新交互_。但是,显然代价是项目现在被缓存了。只要您使用迭代器块,就无法避免这种权衡。
  • 它不会缓存每个项目,直到你到达它,但要点仍然是该方法的内存占用是 O(n) 其中“n”是已迭代的项目数,所以远的。这意味着如果您迭代整个事情,您现在已经将序列的全部内容一次全部放入内存中。如果您最终在大多数情况下迭代了所有/大部分序列,那么您实际上只调用ToList 并没有任何收获。如果您是 1) 从多个线程/源同时迭代它的优势 2) 经常只迭代它的一小部分。
猜你喜欢
  • 1970-01-01
  • 2018-01-23
  • 1970-01-01
  • 2012-03-28
  • 1970-01-01
  • 2010-09-22
  • 1970-01-01
  • 1970-01-01
  • 2011-04-20
相关资源
最近更新 更多