.Except / Yield return 内存不足异常
.Except / Yield return out of memory exception
我使用 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。
我同意,这是一些非常令人惊讶的行为。在这种情况下,我也不会期望 Except
到 运行 内存不足。
但如果您查看 reference source for the Enumerable class,您就会明白原因。除了(通过 ExceptIterator 辅助方法):
- 创建一个新的
Set<T>
- 迭代第二个列表并将其元素添加到集合中
- 迭代第一个列表,并为每个元素:
- 尝试将其添加到集合中
- 如果它还不存在,yield returns它
所以它不 只是 做一个 "except",它也隐含地做一个 "distinct"。为了做到这一点 "distinct",它也将 first 列表的所有元素添加到集合中......所以有了一个巨大的第一个列表,是的,你会消耗大量内存。
我原以为它会在第二个循环中执行 "contains",而不是 "add"。我也没有在 documentation 中看到这种行为。我看到的最接近的是描述:
Produces the set difference of two sequences by using the default equality comparer to compare values.
如果将其参数视为集合,那么删除重复项确实有意义,因为集合就是这样做的。但这不是我从方法名称中预测到的东西!
无论如何,您最好的选择可能是摆脱您的 Except
,而是将 Last
捕获到一个变量中,然后执行 Where(value => value != last)
.
当你在 numbers.Any()
的 i==10
时,"numbers" 不是从 10 到 1000 万的数字列表,而是:
Enumerable.Range(1, 10000000)
.Except({0})
.Except({1})
.Except({2})
// (etc)
.Except({9})
所有这些 "Excepts" 都有自己的哈希集,这些哈希集非常活跃。所以没有什么可以垃圾收集的。
您必须添加一些 .ToList()
才能真正执行这些异常并给垃圾收集器一些机会。
我使用 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。
我同意,这是一些非常令人惊讶的行为。在这种情况下,我也不会期望 Except
到 运行 内存不足。
但如果您查看 reference source for the Enumerable class,您就会明白原因。除了(通过 ExceptIterator 辅助方法):
- 创建一个新的
Set<T>
- 迭代第二个列表并将其元素添加到集合中
- 迭代第一个列表,并为每个元素:
- 尝试将其添加到集合中
- 如果它还不存在,yield returns它
所以它不 只是 做一个 "except",它也隐含地做一个 "distinct"。为了做到这一点 "distinct",它也将 first 列表的所有元素添加到集合中......所以有了一个巨大的第一个列表,是的,你会消耗大量内存。
我原以为它会在第二个循环中执行 "contains",而不是 "add"。我也没有在 documentation 中看到这种行为。我看到的最接近的是描述:
Produces the set difference of two sequences by using the default equality comparer to compare values.
如果将其参数视为集合,那么删除重复项确实有意义,因为集合就是这样做的。但这不是我从方法名称中预测到的东西!
无论如何,您最好的选择可能是摆脱您的 Except
,而是将 Last
捕获到一个变量中,然后执行 Where(value => value != last)
.
当你在 numbers.Any()
的 i==10
时,"numbers" 不是从 10 到 1000 万的数字列表,而是:
Enumerable.Range(1, 10000000)
.Except({0})
.Except({1})
.Except({2})
// (etc)
.Except({9})
所有这些 "Excepts" 都有自己的哈希集,这些哈希集非常活跃。所以没有什么可以垃圾收集的。
您必须添加一些 .ToList()
才能真正执行这些异常并给垃圾收集器一些机会。