【问题标题】:Linq memory questionLinq 内存问题
【发布时间】:2011-05-17 08:08:29
【问题描述】:

由于我对linq比较陌生,所以我想问一下我对以下示例的理解是否正确。

假设我有大量的动物名称(10 万条记录),我想归档它们并以非常耗时的方法(2 周)处理过滤后的项目。 RunWithLinq() 和 RunWithoutLinq() 方法完全一样。

是不是使用第一种方法,原始(大)集合在离开该方法后将留在内存中,并且不会被GC 触及,而使用linq-less 方法,该集合将被@删除987654324@?

我将不胜感激。

class AnimalProcessor
{
    private IEnumerable<string> animalsToProcess;
    internal AnimalProcessor(IEnumerable<string> animalsToProcess)
    {
        this.animalsToProcess = animalsToProcess;
    }
    internal void Start()
    {
        //do sth for 2 weeks with the collection
    }
}
class Program
{
    static void RunWithLinq()
    {
        var animals = new string[] { "cow", "rabbit", "newt", "ram" };
        var filtered = from animal in animals
                       where animal.StartsWith("ra")
                       select animal;
        AnimalProcessor ap = new AnimalProcessor(filtered);
        ap.Start();
    }
    static void RunWithoutLinq()
    {
        var animals = new string[] { "cow", "rabbit", "newt", "ram" };
        var filtered = new List<string>();
        foreach (string animal in animals)
            if(animal.StartsWith("ra")) filtered.Add(animal);
        AnimalProcessor ap = new AnimalProcessor(filtered);
        ap.Start();
    }
}

【问题讨论】:

  • 最初是什么让您产生这种印象?
  • @Jay 当我使用调试器时,它直到使用结果才进入查询,这是我之前不知道的/。
  • animals会在non-linq方式中被收集吗?可能(可能是天真的)理解是,局部变量在函数的右括号之后超出范围(并且可以被收集),即在 ap.Start() 两周后返回之后(除非它真的异步运行,至案例将此评论视为空)。 =)
  • @Jens,一旦不再使用对象,即使它仍在范围内,也可以进行收集。范围适用于源代码,但 GC 规则适用于实际使用。更多信息请参见我的回答。

标签: c# linq memory-management


【解决方案1】:

好吧,animals 将在每个方法结束时有资格被收集,所以严格来说你的陈述是错误的。 animals 在非 LINQ 情况下更早有资格被收集,因此您的陈述的要点是正确的。

确实每个人的内存使用都不同。然而,这里有一个暗示,LINQ 在内存使用方面通常更差,而实际上它通常允许比其他类型的方法更好的内存使用(尽管有非 LINQ 方法可以做到这一点) LINQ 方式,当我使用 .NET2.0 时,我非常喜欢使用相同的基本方法来解决这个特定问题)。

让我们考虑这两种方法,首先是非 LINQ:

var animals = new string[] { "cow", "rabbit", "newt", "ram" };
var filtered = new List<string>();
foreach (string animal in animals)
//at this point we have both animals and filtered in memory, filtered is growing.
    if(animal.StartsWith("ra")) filtered.Add(animal);
//at this point animals is no longer used. While still "in scope" to the source
//code, it will be available to collection in the produced code.
AnimalProcessor ap = new AnimalProcessor(filtered);
//at this point we have filtered and ap in memory.
ap.Start();
//at this point ap and filtered become eligible for collection.

有两点值得注意。一个“有资格”收集并不意味着收集将在那个时候发生,只是它可以在未来的任何时候发生。第二,如果对象不再被使用(甚至在某些情况下被使用,但这是另一个细节级别),则可以在对象仍在范围内时进行收集。范围规则与程序源相关,是程序编写时可能发生的事情(程序员可以添加使用该对象的代码),GC 收集资格规则与编译的程序相关,并且是当程序被编写时发生的事情。程序已编写(程序员本可以添加这样的代码,但他们没有)。

现在让我们看看 LINQ 案例:

var animals = new string[] { "cow", "rabbit", "newt", "ram" };
var filtered = from animal in animals
               where animal.StartsWith("ra")
               select animal;
// at this pint we have both animals and filtered in memory.
// filtered defined as a class that acts upon animals.
AnimalProcessor ap = new AnimalProcessor(filtered);
// at this point we have ap, filtered and animals in memory.
ap.Start();
// at this point ap, filtered and animals become eligible for collection.

所以在这种情况下,直到最后都无法收集任何相关对象。

但是,请注意filtered 绝不是一个大对象。在第一种情况下,filtered 是包含 0 到 n 个对象的列表,其中 n 是 animals 的大小。在第二种情况下,filtered 是一个可以根据需要在animals 上工作的对象,并且其本身具有基本恒定的内存。

因此,非 LINQ 版本的内存使用峰值更高,因为在某个点animals 仍然存在并且filtered 包含所有相关对象。由于animals的大小随着程序的变化而增加,实际上最有可能首先遇到严重内存短缺的是非LINQ版本,因为非LINQ的峰值内存使用状态更差案例。

要考虑的另一件事是,在我们有足够多的项目来担心内存消耗的实际案例中,就好像我们的源不会是一个列表。考虑:

IEnumerable<string> getAnimals(TextReader rdr)
{
  using(rdr)
    for(string line = rdr.ReadLine(); line != null; line = rdr.ReadLine())
      yield return line;
}

此代码读取一个文本文件并一次返回每一行。如果每一行都包含动物的名字,我们可以使用它而不是var animals 作为filtered 的来源。

在这种情况下,尽管 LINQ 版本的内存使用量非常少(一次只需要一个动物名称在内存中),而非 LINQ 版本的内存使用量要大得多(将每个动物名称加载为“ ra" 进入记忆,然后再采取进一步行动)。 LINQ 版本最多也将在几毫秒后开始处理,而非 LINQ 版本必须先加载所有内容,然后才能完成单个工作。

因此,LINQ 版本可以愉快地处理千兆字节的数据,而无需使用比处理少量数据更多的内存,而非 LINQ 版本会遇到内存问题。

最后,重要的是要注意,这与 LINQ 本身没有任何关系,至于使用 LINQ 的方法与不使用 LINQ 的方法之间的差异。要使 LINQ 等效于非 LINQ,请使用:

var filtered = (from animal in animals
                   where animal.StartsWith("ra")
                   select animal).ToList();

使非 LINQ 等效于 LINQ 使用

var filtered = FilterAnimals(animals);

您还定义的地方:

private static IEnumerable<string> FilterAnimals(IEnumerable<string> animals)
{
  foreach(string animal in animals)
    if(animal.StartsWith("ra"))
      yield return animal;
}

它使用 .NET 2.0 技术,但即使使用 .NET 1.1(尽管代码更多)也可以创建从 IEnumerable 派生的对象

【讨论】:

  • 感谢您提供非常全面的解释,尤其是关于峰值使用量和范围的说明。
  • 如果不全面,写起来就没那么有趣了:)
【解决方案2】:

基于 LINQ 的方法会将原始集合保存在内存中,但不会存储带有过滤项的单独集合。

要更改此行为,请致电.ToList()。

【讨论】:

    【解决方案3】:

    是的,没错——因为filtered 变量本质上是查询,而不是查询的结果。对其进行迭代将每次重新评估查询。

    如果你想让它们一样,你可以打电话给ToList:

    var filtered = animals.Where(animal => animal.StartsWith("ra"))
                          .ToList();
    

    (我已将其从查询表达式语法转换为“点表示法”,因为在这种情况下它更简单。)

    【讨论】:

    • 所以如果我使用 yield return 并返回 IEnumberable 的方法而不是查询,情况也会如此?
    • @Andrzej,请注意,使用基于列表的方法(无论是否为 LINQ)会增加峰值内存使用量,因为使用该方法时 filtered 的大小会更大。
    猜你喜欢
    • 2022-10-17
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-01-11
    相关资源
    最近更新 更多