【问题标题】:How to decide between lambda iteration and normal loop?如何在 lambda 迭代和正常循环之间做出决定?
【发布时间】:2016-10-31 17:21:20
【问题描述】:

自从他引入 Java 8 以来,我就深深地迷上了 lambda,并开始尽可能地使用它们,主要是为了开始习惯它们。最常见的用法之一是当我们想要迭代并作用于对象集合时,在这种情况下,我要么求助于forEachstream()。我很少写旧的for(T t : Ts) 循环,我几乎忘记了for(int i = 0.....)

但是,前几天我们和我的主管讨论了这个问题,他告诉我 lambda 并不总是最好的选择,有时会影响性能。从我看到的关于这个新特性的讲座中,我感觉到 lambda 迭代总是被编译器完全优化,并且会(总是?)比裸迭代更好,但他不同意。这是真的?如果是,我如何区分每种情况下的最佳解决方案?

P.S:我不是谈论建议申请parallelStream 的情况。显然那些会更快。

【问题讨论】:

  • 如果 CPU 请求和/或流大小不大,并行流可能比正常循环慢。线程的管理成本很高。
  • 流框架的开销可能令人惊讶。测量!
  • 确实,因此我更新了我的问题。
  • 使用您认为更容易编写/理解的那个。这在不同的时间会有所不同,您了解如何更好地使用 lambda。

标签: java lambda jvm java-8 iteration


【解决方案1】:

性能取决于很多因素,很难预测。通常,我们会说,如果您的主管声称存在绩效问题,您的主管负责解释什么问题

有人可能害怕的一件事是,在幕后,会为每个 lambda 创建站点(使用当前实现)生成一个类,因此如果相关代码只执行一次,这可能会被视为浪费的资源。这与 lambda 表达式作为普通命令式代码具有更高的初始化开销这一事实相一致(我们在这里不与内部类进行比较),因此在只运行一次的类初始化器中,您可以考虑避免它。这也符合您应该never use parallel streams in class initializers 的事实,因此无论如何这里都没有这种潜在优势。

对于可能被 JVM 优化的普通、频繁执行的代码,不会出现这些问题。正如您所猜想的那样,为 lambda 表达式生成的类得到与其他类相同的处理(优化)。在这些地方,对集合调用 forEach 可能会比 for 循环更高效

Iterator 或 lambda 表达式创建的临时对象实例可以忽略不计,但是可能值得注意的是,foreach 循环将始终创建一个Iterator 实例,而lambda expression do not always do。尽管Iterable.forEachdefault 实现也会创建Iterator,但一些最常用的集合会借此机会提供专门的实现,尤其是ArrayList

ArrayListforEach 基本上是一个数组上的for 循环,没有任何Iterator。然后它将调用Consumeraccept 方法,这将是一个生成的类,其中包含对包含您的lambda 表达式代码的合成方法的简单委托。为了优化整个循环,优化器的范围必须跨越数组上的ArrayList 循环(优化器可识别的常见习语),包含琐碎委托的合成accept 方法和包含实际委托的方法代码。

相比之下,当使用 foreach 循环迭代同一个列表时,会创建一个 Iterator 实现,其中包含 ArrayList 迭代逻辑,分布在两个方法上,hasNext()next() 以及 @ 的实例变量987654343@。循环将重复调用 hasNext() 方法来检查结束条件 (index<size) 和 next() 在返回元素之前重新检查条件,因为不保证调用者会这样做在next() 之前正确调用hasNext()。当然,优化器能够消除这种重复,但这比一开始就没有它需要更多的努力。因此,要获得与forEach 方法相同的性能,优化器的范围必须跨越您的循环代码、非平凡的hasNext() 实现和非平凡的next() 实现。

类似的事情也可能适用于具有专门的forEach 实现的其他集合。这也适用于 Stream 操作,如果源提供了专门的 Spliterator 实现,它不会将迭代逻辑分布在两个方法上,例如 Iterator

因此,如果您想讨论 foreach 与 forEach(…) 的技术方面,您可以使用这些信息。

但如上所述,这些方面仅描述了潜在的性能方面,因为优化器的工作和其他运行时环境方面可能会完全改变结果。我认为,根据经验,循环体/动作越小,forEach 方法就越合适。这与避免过长的 lambda 表达式的准则完美协调。

【讨论】:

    【解决方案2】:

    这取决于具体的实现。

    通常forEach 方法和foreach 循环Iterator 通常具有非常相似的性能,因为它们使用相似的抽象级别。 stream() 通常速度较慢(通常降低 50-70%),因为它增加了另一个级别,提供对底层集合的访问。

    stream() 的优点通常是可能的并行性和易于链接的操作与 JDK 提供的许多可重用的操作。

    【讨论】:

    • 言归正传,所以List.forEach 没有性能损失,可惜没有List.stream/().forEach 的表达能力。
    • 我不同意流慢 50-70% 的说法。在大多数情况下,这些数字源于测量首次开销的不正确基准。
    猜你喜欢
    • 2013-12-30
    • 1970-01-01
    • 2021-01-11
    • 1970-01-01
    • 2012-09-02
    • 2016-02-06
    • 2017-07-25
    • 2020-08-14
    • 2011-05-19
    相关资源
    最近更新 更多