【问题标题】:How does java stream.peek() affect the bytecode?java stream.peek() 如何影响字节码?
【发布时间】:2016-08-04 16:28:41
【问题描述】:

据我所知,如果我有一个带有两个过滤器的流,它们将在字节码中与 && 组合。

例如

IntStream.range(1,10)
  .filter(i -> i % 2 == 0)
  .filter(i -> i % 3 == 0)
  .sum();

应该是 i % 2 == 0 && i % 3 == 0。

peek 会影响这个吗?

如果您在第一个文件管理器之后偷看,您会得到 2468,如果您在第二个文件管理器之后偷看,您只会得到 6(当然)。

但是如果你同时看两个地方

IntStream.range(1,10)
            .filter(integer -> integer % 2 == 0)
            .peek(i-> System.out.print(i))
            .filter(integer -> integer % 3 == 0)
            .peek(i-> System.out.print(i))
            .sum();

你得到 24668。

我的假设是,这一定意味着操作在某种程度上是由于 peek 调用而分离的。类似的东西

if(i%2==0)
  peek
  if(i%3==0)

这是真的吗?如果是的话,它会影响性能吗(我认为不会)。

【问题讨论】:

    标签: java performance java-8 java-stream bytecode


    【解决方案1】:

    Stream API 是一个ordinary Java API,如您所见。 It’s filter method 接收一个任意的 Predicate 实例,无论是通过 lambda 表达式还是普通的 class(或 enum 来命名所有可能性)实现。

    如果您随后调用filter 两次,则底层实现可以通过调用Predicate.and 将它们加入单个过滤器,但在实现谓词的情况下是否这样做没有任何后果通过 lambda 表达式。

    与自定义 Predicate 实现不同,它可以覆盖 and 方法并在识别第二个 Predicate 实现的情况下提供优化的东西,为 lambda 表达式生成的类不会覆盖任何 default 方法,但是只是一个abstract 函数方法,这里是Predicate.test,所以在这种情况下,调用and 将得到default 方法返回的内容,一个新的Predicate 包含对两个源谓词的引用并将它们组合在一起,很像不使用 Predicate.and 的 Stream 实现。

    因此,这些可能的实现之间没有实质性差异,如果您在中间插入另一个操作,例如传递给 peekConsumer,则没有实质性差异。当然,它现在比没有这个动作做得更多,所以它性能影响,但与谓词无关。

    但您普遍的误解似乎是您认为:

    for(int i=1; i<10; i++) {
        if(i%2==0 && i%3==0)
            System.out.print(i);
    }
    

    for(int i=1; i<10; i++) {
        if(i%2==0) {
            System.out.print(i);
            if(i%3==0)
                System.out.print(i);
        }
    }
    

    看看编译方法的字节码:

    //  first variant            second variant
      0: iconst_1              0: iconst_1
      1: istore_1              1: istore_1
      2: iload_1               2: iload_1
      3: bipush        10      3: bipush        10
      5: if_icmpge     33      5: if_icmpge     40
      8: iload_1               8: iload_1
      9: iconst_2              9: iconst_2
     10: irem                 10: irem
     11: ifne          27     11: ifne          34
                              14: getstatic     #2    // Field java/lang/System.out:Ljava/io/PrintStream;
                              17: iload_1
                              18: invokevirtual #3    // Method java/io/PrintStream.print:(I)V
     14: iload_1              21: iload_1
     15: iconst_3             22: iconst_3
     16: irem                 23: irem
     17: ifne          27     24: ifne          34
     20: getstatic     #2     27: getstatic     #2    // Field java/lang/System.out:Ljava/io/PrintStream;
     23: iload_1              30: iload_1
     24: invokevirtual #3     31: invokevirtual #3    // Method java/io/PrintStream.print:(I)V
     27: iinc          1, 1   34: iinc          1, 1
     30: goto          2      37: goto          2
     33: return               40: return
    

    如您所见,插入 print 语句会导致插入 print 语句,仅此而已。或者,换句话说,&amp;&amp; 运算符与两个嵌套的if 语句并没有什么不同。两者在语义上和字节码中的作用完全相同。

    这同样适用于 Stream API 的使用,但代码会更复杂,因为条件表达式表示为 Predicate 实例,而插入的语句是 Consumers。但在最好的情况下,HotSpot 优化器将为 Stream 变体生成与循环变体完全相同的优化本机代码。

    【讨论】:

    • 我认为 lambda 被编译为合成方法,而不是类(指“为 lambda 表达式生成的类”)。哪个是正确的?
    • 感谢您的详细回答,这正是我想知道的。我希望两者具有相同的性能,我只是不确定我的假设是否正确。看起来是这样,但现在我对原因有了更深入的了解。谢谢!
    • @erickson: lambda 表达式被编译为合成方法,JRE 将为其生成一个类,该类实现功能接口并调用该合成方法.生成的类的许多属性是故意未指定的,但它们没有覆盖default 方法的事实是fixed in JLS§15.27.4:“该类没有覆盖目标功能接口类型或其他接口类型的其他方法上面提到过,虽然它可能会覆盖Object类的方法
    • 好吧,这很有道理。我意识到必须有一个对象来实现接口,但我不确定是否有某种反射/动态代理实现,或者实际上是编译时的合成类。
    【解决方案2】:

    它根本没有在字节码级别进行优化。每个 lambda 是一个单独的方法。 Java 依赖 JVM 在运行时透明地优化一切。

    【讨论】:

      猜你喜欢
      • 2018-02-10
      • 2018-04-25
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2011-01-17
      相关资源
      最近更新 更多