【问题标题】:.Except / Yield return out of memory exception.Except / Yield 返回内存不足异常
【发布时间】:2017-09-11 12:09:25
【问题描述】:

我正在使用 Linq 的 except 语句耗尽内存。

例子:

var numbers = Enumerable.Range(1, 10000000);
int i=0;
while (numbers.Any())
{
    numbers = numbers.Except(new List<int> {i++});
}

我反编译了这个方法,这也是一样的,但也会出现内存不足的异常。

var numbers = Enumerable.Range(1, 10000000);
int i=0;
while (numbers.Any())
{
   numbers = CustomExcept(numbers, new List<int>{i++});
}
private IEnumerable<int> CustomExcept(IEnumerable<int> numbers, IEnumerable<int> exceptionList)
{
    HashSet<int> set = new HashSet<int>();
    foreach (var i in exceptionList)
    {
        set.Add(i);
    }
    foreach (var i in numbers)
    {
        if (set.Add(i))
        {
           yield return i;
        }
    }
}

所以我的问题是:为什么这会引发内存不足异常?

我希望垃圾收集器清理未使用的 HashSet。

【问题讨论】:

  • 您应该了解延迟执行的影响(通过yield 实现)。每一步都执行前面的所有步骤,从Range 和所有Excepts 开始。并做了两次(因为Any 调用)。
  • 更新了示例

标签: c# linq out-of-memory yield-return


【解决方案1】:

当您在 numbers.Any() 中的 i==10 时,“数字”不是从 10 到 1000 万的数字列表,而是:

Enumerable.Range(1, 10000000)
   .Except({0})
   .Except({1})
   .Except({2})
   // (etc)
   .Except({9})

所有这些“例外”都有自己的非常活跃的哈希集。所以没有什么可以垃圾收集的。

您必须添加一些 .ToList()s 才能真正执行这些异常并给垃圾收集器一些机会。

【讨论】:

    【解决方案2】:

    我同意,这是一些非常令人惊讶的行为。在这种情况下,我也不希望 Except 内存不足。

    但是,如果您查看reference source for the Enumerable class,您就会明白原因。除外(通过 exceptIterator 辅助方法):

    • 创建一个新的Set&lt;T&gt;
    • 迭代第二个列表并将其元素添加到集合中
    • 迭代第一个列表,并为每个元素:
      • 尝试将其添加到集合中
      • 如果它不存在,yield 返回它

    所以它不是只是做一个“例外”,它也隐含地做一个“不同”。为了做到这一点,它也将 first 列表的所有元素添加到集合中......所以对于一个巨大的第一个列表,是的,你会消耗大量的内存.

    我希望它在第二个循环中执行“包含”,而不是“添加”。我也没有在documentation 中看到这种行为。我看到的最接近的是描述:

    通过使用默认相等比较器比较值来产生两个序列的集合差。

    如果它将其参数视为集合,那么删除重复项确实有意义,因为这就是集合的作用。但这不是我从方法名称中预料到的!

    无论如何,您最好的选择可能是摆脱您的Except,而是将Last 捕获到一个变量中,然后执行Where(value =&gt; value != last)

    【讨论】:

    • 我还期望包含而不是添加。但是为什么 GC 在每次 while 循环迭代后不清理这些未使用的集合呢?
    • @Carra 在循环的每次迭代中没有集合。它们甚至还没有被创建。 while 循环的主体只是构造一个查询,它定义了如何执行 Except ,它没有执行它。
    • 除另一个 HashSet 或 Dictionary 之外的任何可枚举项上的 .Contains() 都需要对“外部”可枚举项中的每个项目进行全面扫描。 `在几乎所有情况下,除了性能和内存使用方面的行为都更好 - 只扫描 Enumerable 一次,从中创建一个哈希集并重用它。
    • 要改进此代码,必须检测“外部”可枚举是另一个Except 操作的产物。 Core 团队已经在 .NET Core 2.0 中使用 Concat.cs 完成了这项工作
    猜你喜欢
    • 1970-01-01
    • 2010-10-05
    • 1970-01-01
    相关资源
    最近更新 更多