【问题标题】:IEnumerable from DBContext vs IEnumerable from code Memory usage来自 DBContext 的 IEnumerable 与来自代码的 IEnumerable 内存使用情况
【发布时间】:2019-10-17 12:13:43
【问题描述】:

我对 IEnumerable 内存使用问题感到困惑,尤其是比较来自 DB 的 IEnumerable 数据源和来自代码 yield return const 值的 IEnumerable 数据源。

我有一个Memory 函数来检查内存使用情况。

        static string Memory()
        {
            return (Process.GetCurrentProcess().WorkingSet64 / (1024 * 
                    1024)).ToString();
        }
  1. 所以这里我使用的是EF CORE 3.0,一共访问了150000条记录的表
            using DataContext context = new DataContext();

            Console.WriteLine(Memory()); //21

            IEnumerable<User> users = context.Users;
            foreach (var i in users) {}

            Console.WriteLine(Memory());//101
            Console.WriteLine(GC.GetTotalMemory(true));//46620032

由于某种原因我无法上传图片,所以我需要输入结果,对此感到抱歉。(结果在代码中为 cmets)。

  1. 下一个示例是使用yield return 生成 IEnumerable 数据。
        static IEnumerable<User> Generator(int max)
        {
            for (int i = 0; i < max; i++)
            {
                yield return new User { Id = 1, Name = "test" };
            }
        }

这是结果

            Console.WriteLine(Memory());// 21

            IEnumerable<User> users = Generator(150000);
            foreach (var i in users){}

            Console.WriteLine(Memory());// 24
            Console.WriteLine(GC.GetTotalMemory(true)); // 658040

现在,我对示例 1 和示例 2 感到非常困惑。我的理解是,对于 IEnumerable 数据源,它会一次读取一个,而不是整个集合,因此它可以减少内存使用量,就像示例 2. 但是,在使用 EF CORE 时(我知道这不是 EF CORE 特有的,但我需要一个具体的例子。),我认为它仍然是一一拉,但我的问题是它为什么使用比第二个例子更多的内存。那么它是不是一个一个地拉着每一个记录呢?最后,我将数据库中的所有记录都保存在内存中,对吗?但是为什么第二个使用这么少的内存呢?我正在产生相同的记录。如果有人可以解释这一点,我们将不胜感激。谢谢!!!

【问题讨论】:

  • EF Core 不会一一拉取记录,实际上它返回的是IQueryable 而不是IEnumerable
  • @VidmantasBlazevicius 当我迭代集合时它必须是 IEnumerable 不能是 IQueryable 。由于iqueryableienumerable 都被延迟执行,因此它正在一一拉动。
  • IQueryable extends IEnumerable,所以你在IEnumerable上做的任何事情你也可以在IQueryable上做。

标签: c# .net entity-framework .net-core entity-framework-core


【解决方案1】:

这确实是称为(更改)跟踪的 EF(核心)特定行为,在 Tracking vs. No-Tracking Queries 中进行了解释。请注意,如果您不明确更改跟踪,则默认行为是跟踪

context.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;

或在查询源上使用AsNoTracking()

重要的是,即使查询结果被一一评估,DbContext 实例也会将每个创建的实体实例以及一些额外的信息(如状态和原始值的快照)添加到一些内部列表中。因此,即使没有键、状态和原始值快照,生成器的等效代码也会是这样的:

IEnumerable<User> users = Generator(150000);
var trackedUsers = new List<User>();
foreach (var i in users)
{
    trackedUsers.Add(i);
}

因此,在循环结束时,您会将迭代期间创建的所有实例都存储在内存中。

这就是为什么您可以考虑使用AsNoTracking 选项的原因,以防万一您需要它来执行实体查询并对其进行一次迭代。请注意,非实体(投影)查询和无密钥实体不会跟踪其结果,因此这实际上是实体查询特定的行为。

【讨论】:

  • 我相信这是我问题的正确答案,我会尽快尝试证明这一点。
  • 我添加了AsNoTracking,内存急剧下降,但仍然和yield return不一样,也许这是因为ef core内部的东西?(它仍然在做一些不能做的事情优化)
  • 我也只是想仔细检查一下,ef 是一个一个地拉记录,对吗?而不是一次加载整个集合。甚至我做var users = context.Users.ToList();都会有一个sql查询,一个一个拉,然后把记录保存到内存中,对吗?
  • 查询执行的结果是ADO.NET数据读取器,然后被一一迭代,物化成对象。见How Query Worksvar users = context.Users.ToList(); 只是 var users = new List&lt;User&gt;(); foreach (var user in context.Users) users.Add(user); 的快捷方式,所以是的,这是一个查询,然后在内存中创建和保存对象。
  • 哦,所以即使是无跟踪查询 ef 仍然有一些检查和跟踪结果,所以这可能是内存使用率仍然比yield return 高一点的原因?你觉得呢?
【解决方案2】:

在您的代码中,一旦您执行了 foreach 语句,EF 就会进入数据库并将 所有 记录提取到内存并枚举结果。同理:

var list = context.Users.ToList();
foreach (user u in list)
{
}

如果不知道 User 类是如何定义的,就很难说为什么内存消耗是这样的(我并不是说它太高了),但是一旦你在 EF 中获得了实体,就会有很多事情发生在幕后,比如更改跟踪,会消耗内存。

顺便说一句,IQueryable 是 IEnumerable

public interface IQueryable<out T> : IEnumerable<T>, IEnumerable, IQueryable

【讨论】:

    【解决方案3】:

    我的理解是对于 IEnumerable 数据源,它会一次读取一个,而不是整个集合,所以它可以像示例 2 一样减少内存使用。

    这不适用于 Linq-to-Entities。它将运行查询以获取 所有 数据,并且只允许您在加载后对其进行迭代。

    可能在分页等方面可能由某些提供商进行优化,但通常 EF 不会从数据库中“一个接一个”地提取记录。数据将存储在上下文中,这会增加内存开销。如果您在使用完上下文后将其处理掉(这是一种最佳做法),您可能会看到内存急剧减少。

    但是为什么第二个使用这么少的内存呢?

    因为在循环中,你创建了一个对象,返回它,然后什么都不做。所以每个对象都可以很快地进行垃圾回收,因此使用的总内存会更少。另外,您没有 DbContext 的开销(应该不会很大)

    请注意,垃圾收集不是确定性的。在适当的情况下,可能不会收集任何内容,并且您会看到第二个循环使用了更多的内存。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2017-01-31
      • 2021-05-25
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多