在性能 Lambda 或简单循环方面哪个更好? [复制]

Posted

技术标签:

【中文标题】在性能 Lambda 或简单循环方面哪个更好? [复制]【英文标题】:Which would be better in terms of performance Lambda or simple loop? [duplicate] 【发布时间】:2015-10-18 22:48:55 【问题描述】:

我已经快速阅读了Oracle Lambda Expression 文档。

不过,这种例子帮助我更好地理解:

//Old way:
List<Integer> list = Arrays.asList(1, 2, 3, 4, 5, 6, 7);
for(Integer n: list) 
System.out.println(n);


//New way:
List<Integer> list = Arrays.asList(1, 2, 3, 4, 5, 6, 7);
list.forEach(n -> System.out.println(n));


//or we can use :: double colon operator in Java 8
list.forEach(System.out::println);

不过,我还是不明白为什么这是一项创新。这只是“方法变量”结束时死亡的方法,对吗?为什么我要使用这个而不是真正的方法? 就性能而言,这将是更好的选择。 Lambda 或简单循环。

【问题讨论】:

就性能而言,您的 println 占用大约 1K * 到 10K * 其余代码的 CPU。如果您消除了 println,您仍然会发现代码尚未预热的事实可能意味着 50 倍的性能差异。如果您预热您的代码,您可能会发现您使用List&lt;Integer&gt; 而不是int[] 是您的下一个最大热门......您离需要知道 Stream 或 loop 是否更快。 @PeterLawrey 我同意,如果你想提高性能,你必须仔细查看你的代码。你甚至必须决定这一切是否有必要。但据我了解 OP,这不是关于这个特定的“示例”,而是 lambda 和循环之间的整体差异。 @Olli1511 好点,但我想说的是,对于广泛的示例,差异是 a) 不太重要,b) 可能会根据您使用的 Java 8 更新而改变。恕我直言,您应该编写您认为最清晰的代码,并在测量出您遇到问题时担心性能。 @PeterLawrey 我完全同意你的观点。我只是在想有人因为标题而访问这个问题。我会期待关于这两种方法的性能的答案。事件虽然,正如您所提到的,对于 99.9% 的访问者来说,考虑性能差异是没有意义的,但如果它确实在 0.01% 中很重要(我可以'没有想到一个场景,但可能有一个)?这些访问者不应该得到他们问题的答案,而不仅仅是“不在乎”吗? @Olli1511 没有一个在所有情况下都是正确的广泛答案。答案是视情况而定。我见过使用 lambda 比较慢和比较快的情况。我还看到,通过更新的更新,您可以获得更优化的代码。 【参考方案1】:

我的建议是:

    使用您和您的同事同意最易于维护的样式。

    如果您和您的同事对 lambdas 还不够熟悉,请继续学习。

    不要沉迷于性能。这通常不是最重要的。

一般来说,lambda 和流提供了一种更简洁且(一旦每个人都掌握了速度)更易读的方式来表达这种算法。性能不是主要目标。

如果性能确实成为问题,那么标准建议是编码、测试、基准测试、分析和优化。并按此顺序执行!通过在编码阶段进行优化或优化对整体应用程序性能影响最小的代码,您很容易浪费大量时间。

让应用程序基准测试告诉您是否需要优化。 让分析器指出代码中值得优化的部分。

在这个具体示例中,性能差异将太小而无法衡量。如果您扩大到包含数百万个元素的列表,则性能将取决于构建列表和写入数字所花费的时间。不同的迭代方式只会对整体性能贡献很小的一部分。


对于那些(尽管有上述所有)仍然想知道使用 lambda 循环还是传统循环更快的人来说,最好的一般答案是:

“这取决于各种因素,1) 没有被很好地理解,2) 随着 Java 编译器技术的发展而变化。

我们可以针对特定 Java 主要/次要/补丁版本的特定示例为您提供答案,但一概而论是不明智的。

【讨论】:

【参考方案2】:

我为什么要使用这个而不是真正的方法?

你不应该。使用你更喜欢的方法。

至于性能,我猜,所有这些版本的速度都差不多。这里的 I/O 操作 (println) 比调用 lambda 或创建迭代器的所有可能开销要慢得多。一般来说,forEach 可能会稍快一些,因为它在单个方法中完成所有操作,而无需创建 Iterator 并调用 hasNextnext(由 for-each 循环隐式完成)。无论如何,这取决于许多因素,例如您调用此代码的频率、您的列表有多长、JIT 编译器是否设法对迭代器和/或 lambda 进行虚拟化等等。

【讨论】:

【参考方案3】:

它允许您在一行中(同时具有足够的可读性)写一些以前不可能的东西。性能在这里不是问题O(n) 停留在O(n)

例如,在我的 Windows Phone 应用程序中,我有会话,并且在该会话中是表演者,我想选择所有具有一位具体表演者的会话(例如,您想查看某个演员出演的所有电影)。在 Java 1.7 或更低版本中,我必须使用内部循环创建循环,检查它,如果没有执行者则返回 null 等。 使用 lambda 表达式,我可以做到这一点:

//performerId is parameter passed by method before
Sessions.Where(x => x.Performers.Where(y => y.PerformerId == performerId).FirstOrDefault() != null)

现在在 Java 中也是如此(不过我现在不在 1.8 项目上工作,我没有 Java 示例,但我很期待)。

【讨论】:

【参考方案4】:

如果您想了解 lambda 表达式的价值,您不应该只看具有以前存在的语言对应物的唯一新方法。这些例子怎么样:

button.addActionListener( ev -> BACKGROUND_EXECUTOR_SERVICE.execute( () -> 
   String result = longComputation();
   SwingUtilities.invokeLater( () -> label.setText(result) );
);

想想等效的 pre-Java 8 代码的样子,您就会发现这里的主要优势不是性能。

如果你想看Collection操作,这个怎么样?

map.merge(key, 1, Integer::sum);

这会将1 放入映射中,如果该键在映射中尚不存在,否则将1 添加到现有映射的值中。再次,想想老对手的样子。如果映射具有该方法的适当实现,它可能会更有效,因为它避免了多个哈希操作,这只是一个额外的好处。

当然,forEach 方法无法提供如此大的表达能力,因为存在 for-each 语言对应项。尽管如此,如果声明需要重复长类型名称和泛型参数,则不需要声明循环变量可以改进源代码。在Maps 的上下文中尤其如此:

Map<ContainerOrderFocusTraversalPolicy, List<AttributedCharacterIterator>> map;
//…

 

map.forEach((p,l)->l.forEach(i-> /* do your work using p and i */));

在这里,这个新的迭代显然胜出

for(Map.Entry<ContainerOrderFocusTraversalPolicy, List<AttributedCharacterIterator>> e:
                                                                      map.entrySet()) 
    ContainerOrderFocusTraversalPolicy p=e.getKey();
    for(AttributedCharacterIterator i: e.getValue()) 
        /* do your work using p and i */
    

当然,只有实际的工作语句足够小才重要,但这就是 lambda 表达式的使用方式:封装小段代码。还有一些任务不能用这种方式完成,需要一个普通的 for-each 循环,就像有些任务不能用一个 for-each 循环完成,需要更老的 for 循环手动处理Iterator...

【讨论】:

【参考方案5】:

就性能而言,与 lambda 相比,普通函数会更好,因为在 groovy 中存在与 lambda 或多或少相同的闭包。

这些事情的工作方式就像如果您为任何集合编写一个闭包,它将在内部创建另一个类,该类实际上对提到的闭包或 lambda 表达式执行操作。

但是,通过使用 lambda 和闭包,我可以以更好的方式迭代事物,并且可以轻松调试。您可以编写更少的代码行。

【讨论】:

【参考方案6】:

在您的代码中使用 lambda 时,您是在告诉您该做什么而不是如何去做。上面的代码传递了一个方法引用而不是循环遍历列表,我认为 lambda 更符合人体工程学。

就性能而言,打印 100 万个项目的列表几乎花费了相同的时间,但我没有对其他类型的操作进行基准测试。

上面的代码是一个简单的操作,但是 lambda 有很多优点,因为你可以拥有可以传递的函数,使用多个核心(并行流)很容易等等。

【讨论】:

【参考方案7】:

查看Guava documentation 的第一部分。它是关于较旧的 Java 版本,但很重要 - 在任何地方使用 lambdas 实际上可能会降低代码的可读性。更好的例子可能来自 Stream API:

 // Group employees by department
 Map<Department, List<Employee>> byDept
     = employees.stream()
                .collect(Collectors.groupingBy(Employee::getDepartment));

【讨论】:

【参考方案8】:

运行时间和可读性似乎因情况而异,但在我看来,有一个很好的答案

我为什么要使用这个而不是真正的方法?

您可以将 lambda 函数作为变量传递 如果我对此不满意,请纠正我,因为我现在大部分时间都是 Fantom 用户,但您可以将 Lambda 函数作为参数传递给方法调用。例如,如果您构建了一个 sort 方法,它可以将 lambda 参数作为比较器来使用,从而允许您使用单个 sort 方法轻松地比较对象中的不同字段,甚至是完全不同的对象。

【讨论】:

【参考方案9】:
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.TimeUnit;
import org.openjdk.jmh.annotations.Benchmark;
import org.openjdk.jmh.annotations.BenchmarkMode;
import org.openjdk.jmh.annotations.Fork;
import org.openjdk.jmh.annotations.Measurement;
import org.openjdk.jmh.annotations.Mode;
import org.openjdk.jmh.annotations.Scope;
import org.openjdk.jmh.annotations.State;
import org.openjdk.jmh.annotations.Timeout;
import org.openjdk.jmh.annotations.Warmup;

/**
 *
 * @author devb
 */
@BenchmarkMode(Mode.Throughput)
@Fork(value = 1)
@Warmup(iterations = 1, time = 32, timeUnit = TimeUnit.MILLISECONDS)
@Measurement(iterations = 16, time = 1, timeUnit = TimeUnit.SECONDS)
@Timeout(time = 1, timeUnit = TimeUnit.SECONDS)
@State(Scope.Benchmark)
public class CheckLamdaPerformance 

    List<Integer> list = Arrays.asList(1, 2, 3, 4, 5, 6, 7);

    @Benchmark
    public void testPerOld() 
        //Old way:
        for (Integer n : list) 
            System.out.println(n);
        
    

    @Benchmark
    public void testPerNew() 
        //New way:
        list.forEach(n -> System.out.println(n));
    

    @Benchmark
    public void testPerDoubleColon() 
        //or we can use :: double colon operator in Java 8
        list.forEach(System.out::println);
    


测试基准

import java.io.IOException;
import org.openjdk.jmh.Main;
import org.openjdk.jmh.runner.RunnerException;

/**
 *
 * @author devb
 */
public class MyBenchmark 

    private static final String TEST = ".*CheckLamdaPerformance.*"; 

    public static void main(String[] args) throws IOException, RunnerException 
        Main.main(getArguments(TEST, 1, 5000, 1));
    

    private static String[] getArguments(String className, int nRuns, int runForMilliseconds, int nThreads) 
        return new String[]className,
            "-i", "" + nRuns,
            "-r", runForMilliseconds + "ms",
            "-t", "" + nThreads,
            "-w", "5000ms",
            "-wi", "1"
        ;
    


运行后输出为:

# Run complete. Total time: 00:00:34

Benchmark                                  Mode  Cnt     Score   Error  Units
CheckLamdaPerformance.testPerDoubleColon  thrpt        776.008          ops/s
CheckLamdaPerformance.testPerNew          thrpt       1096.423          ops/s
CheckLamdaPerformance.testPerOld          thrpt        873.542          ops/s

【讨论】:

如果您在不写入控制台的情况下运行此测试,它将是微秒或更短。我也会忽略代码运行的前 10,000 次,因为它还没有预热。 @TagirValeev,我试过了,我很惊讶。我该如何解释这种现象。 好多了!不过,您能否为您提供的 jmh 调整参数提供一些理由?最值得注意的是单次迭代热身?在您知道自己在做什么之前,我会删除所有这些调整选项。 调优参数说明见jmhwiki.weebly.com/blog/java-micro-benchmarking @developerbhuwan 我非常清楚这些选项的作用!我想知道您为什么选择这些值 - 对我来说它们似乎很遥远,除非我有充分的理由怀疑 jmh 没有正确执行,否则我会将其保留为默认值。你为什么选择这些价值观?

以上是关于在性能 Lambda 或简单循环方面哪个更好? [复制]的主要内容,如果未能解决你的问题,请参考以下文章

在 Java 流的性能方面哪个更好:组合过滤器或组合环境? [复制]

哪个更好?数组、ArrayList 或 List<T>(在性能和速度方面)

性能方面哪个更好:对象原型还是构造函数本机函数? [复制]

性能方面:在 Laravel 数据库中哪个更好

在其他函数或循环中构造 lambda 时是不是存在性能问题?

哪个更好 - Ext.get() 或 document.getElementById()