【问题标题】:Your preference to 'materializing' IEnumerables?您对“物化”IEnumerables 的偏好?
【发布时间】:2018-11-28 09:56:18
【问题描述】:

有时需要在方法中间实际“评估” IEnumerable,因为它用于多个查询并且编译器发出警告(“IEnumerable 的可能多次枚举”)

var skippedIds = objects.Where(x => x.State=="skip")
                .Select(x => x.Id)
                .Distinct();

            var skippedLookup = skippedIds.ToLookup(x => x.FundId, _ => new { _.Id, _.Name});

            if (skippedIds.Any()) // compiler warning
            {
                ...
                // other iterations over skippedIds, etc.
            }

我曾经这样做过:

var skippedIds = objects.Where(x => x.State=="skip")
                    .Select(x => x.Id)
                    .Distinct()
                    .ToList();
...

但想知道是否有更好的选择。上面的代码在堆上创建了List<T> 对象,我猜这是在方法中死掉的临时变量的上下文中不必要的 GC 负担。 我现在正在使用 ToImmutableArray() 附带的 System.Collections.Immutable 库。 这不仅会创建堆栈分配的对象不是真的,感谢评论员),而且它还在我的代码中附加了“不可变”语义,我猜这是一种很好的函数式风格练习。

但是性能影响是什么?在方法中本地多个地方使用的“具体化”临时子查询结果的最佳方式是什么?

【问题讨论】:

  • ToImmutableArray 比相应的 ToList 方法调用慢几个数量级,所以为了简单起见,我只使用 ToList
  • 如果您真的关心实现结果,那么问题不在于您使用的是List 还是ImmutableArray,而是完全实现结果。您可以在处理结果的foreach 中折叠.Any() 之类的内容(并检查循环设置的标志)。担心 GC 与不担心 GC 已经是一个比大多数代码需要担心的更高级的话题。 (如果您还没有在任何地方发现瓶颈,甚至担心什么时候实现可能就太过分了。)
  • @LasseVågsætherKarlsen 确实如此。例如,编写不佳的 EF 查询可能会不必要地两次访问数据库。但在大多数情况下,这可能没问题。
  • 我对列表进行了简单的转换,将列表转换为不可变数组,并对其进行了基准测试。此处的结果和代码:gist.github.com/lassevk/de70f3ab10b120961820de5fd1fd63b5 - 尽管在我的代码和结果中戳个洞,我可能有问题! 请注意,我没有对生成的集合的使用情况进行基准测试,我只对他们的创作进行了基准测试。
  • 我可以改用IEnumerable,没问题。你是对的,这发生了巨大的变化 - gist.github.com/lassevk/66936cd630ff2e65d78fcac896c2b4a8 - 所以不要介意我原来的评论,当 ToList 不知道实际的底层大小时,ToImmutableArray 似乎比 ToList 快。

标签: c# performance linq memory functional-programming


【解决方案1】:

在内存中实现它的性能影响是:

  • 从数据库中获取所有项目的初始数据 - 如果您不打算使用所有项目,那么您可能会拿走比您需要的更多的东西。
  • 根据您使用的结构,您可能会产生插入成本 - ToImmutableArray() 将与 ToArray() 一样快,因为 ImmutableArray 只是包装了内置数组类型并删除了变异选项。
  • 如果您快速丢弃对象,GC 负担就不那么重要了。因为该项目不太可能从Gen 0 跳转到Gen 1,并且无需太多成本即可收集。但显然,您分配的对象越大,触发集合的可能性就越大。

您可以使用language-ext 中的Seq<A> 类型(披露:我是作者)。它被设计为“更好的可枚举”,因为它只会消耗IEnumerable<A> 中的每个项目一次,并且像IEnumerable<A> 一样懒惰。

所以,你可以这样做:

var skippedIds = objects.Where(x => x.State=="skip")
                        .Select(x => x.Id)
                        .Distinct()
                        .ToSeq();

显然世界上没有免费的东西,Seq<A> 的成本是:

  • 每个消费项目的分配(因为它会记住您已阅读的项目,因此您不会再这样做)。但它们是只有两个引用的微小对象,因此造成的 GC 压力非常小。
  • 保持与数据库的连接的时间超过您可能需要的时间,这可能会导致您的数据库出现其他性能问题:死锁等。

但是好处是你只消耗你需要的东西并且你消耗它一次。就我个人而言,我希望限制您的查询并使用ToImmutableArray(),从数据库中获取少于您需要的内容将始终是首选方法。

【讨论】:

    【解决方案2】:

    在这种特定情况下,问题在于您已经具体化了结果(以Lookup 的形式),但随后又参考了未实现的结果。

    var skippedIds = objects.Where(x => x.State=="skip")
        .Select(x => x.Id)
        .Distinct();
    
    var skippedLookup = skippedIds.ToLookup(x => x.FundId, _ => new { _.Id, _.Name});
    
    if (skippedIds.Any()) // compiler warning
    

    在上面的代码中,skippedIds 没有实现,但skippedLookup 是。因此,您可以考虑更改:

    if (skippedIds.Any()) // compiler warning
    

    到:

    if (skippedLookup.Any()) // no compiler warning
    


    如果我们采用更一般的情况,一些额外的指导:

    • 考虑多重枚举(例如两次访问数据库)与具体化(例如 RAM 使用)的性能成本 - 最好是上下文相关的
    • 考虑使用ToListToImmutableArray 来实现(两者似乎都表现良好)。
    • 考虑是否可以从代码中删除任何 LINQ 操作而不影响整体功能。一个常见的错误是使用Any 然后使用foreach - 在许多情况下,Any 可以被删除,因为如果枚举为空,foreach自动不执行任何操作。
    • 如果 IEnumerable 正在使用 LINQ to Objects 并且您正在执行 Distinct 那么具体化操作(例如 ToList)然后使用 new HashSet<YourTypeHere>(YourEnumerableHere) 。它将一键执行Distinct 和具体化操作。
    • 使用ToList 实现时,请考虑将生成的List 公开为IReadOnlyList,以向消费者表明它并非旨在更改。
    • 实际上,您选择哪种方法并不重要。当然,List 及其底层数组会有一些 GC 开销。但在整体 GC 负载的更广泛背景下(例如 List 包含的对象),这不太可能成为问题。如果列表足够大,则可以涉及大对象堆,这不是最佳的。但老实说,让 GC 完成它的工作。如果有问题,那么优化,而不是之前。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2011-04-11
      • 2012-01-23
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2021-11-21
      相关资源
      最近更新 更多