在字节码级别了解Java 8 Streams

Tim*_*sen 11 java lambda bytecode java-8 java-stream

关于Java 8中的流,有大量关于流的信息和教程.我发现的大部分内容都很好地解释了流的各个元素如何在概念层面上工作.但是,我没有遇到过很多材料,这些材料描述了JVM实际上如何实现和执行流.

考虑比较Collection使用流和使用旧学前Java 8方式的操作.底层的Bytecodes在这两种方法之间看起来是否相同?性能是否相同?

为了使这个具体,请考虑以下示例,其中我需要找到名称中包含单词"fish"的所有鱼,然后将每个匹配鱼的第一个字母大写.(是的,我知道Hagfish实际上并不是一条鱼,但是我用完了匹配的鱼名.)

List<String> fishList = Arrays.asList("catfish", "hagfish", "salmon", "tuna", "blowfish");

// Pre Java-8 solution
List<String> hasFishList = new ArrayList<String>();

for (String fish : fishList) {
    if (fish.contains("fish")) {
        String fishCap = fish.substring(0, 1).toUpperCase() + fish.substring(1); 
        hasFishList.add(fishCap);
    }
}

// Java-8 solution using streams
List<String> hasFishList = fishList.stream()
    .filter(f -> f.contains("fish"))
    .map(f -> f.substring(0, 1).toUpperCase() + f.substring(1))
    .collect(Collectors.toList());
Run Code Online (Sandbox Code Playgroud)

您可能对这两种方法在Bytecode级别下可能有何不同的见解会很有帮助.而一些实际的字节代码会更好.

Mif*_*eet 21

答案随着时间的推移而增长很多,所以我将从摘要开始:

意见

  • 跟踪API实际执行的内容看起来很可怕.许多电话和对象创作.但请注意,对集合中所有元素重复的唯一部分是do-while循环的主体.因此除了一些不断的开销之外,每个元素的开销是大约6个虚方法调用(invokeinterface指令 - 我们的2个lambdas和4个accept()调用接收器).
  • 赋予流API调用的lambda被转换为包含实现和invokedynamic指令的静态方法.它不是创建一个新对象,而是给出了如何在运行时创建lambda的处方.之后在创建的lambda对象上调用lambda方法没有什么特别之处(invokeinterface指令).
  • 您可以观察流如何被懒惰地评估.filter()并将map()它们的操作包装在匿名子类中,StatelessOp而这些子类依次扩展ReferencePipeline,AbstractPipeline最终扩展BaseStream.实际评估在执行时完成collect().
  • 你可以看到流真正使用Spliterator而不是Iterator.注意许多分叉检查isParallel()- 并行分支将利用Spliterator方法.
  • 创建了很多新对象,至少13个.如果在循环中调用此类代码,则可能会遇到垃圾回收问题.对于单次执行,它应该没问题.
  • 我想看看两个版本的基准比较.Streams版本可能会慢一些,与"Java 7版本"的差异随着鱼数量的增加而减少.另见相关的SO问题.


在示例中通过执行Streams使用跟踪

下面的伪代码通过使用流执行版本来捕获跟踪.有关如何读取跟踪的说明,请参阅本文的底部.

Stream stream1 = fishList.stream();
    // Collection#stream():
    Spliterator spliterator = fishList.spliterator();
        return Spliterators.spliterator(fishList.a, 0);
            return new ArraySpliterator(fishList, 0);
    return StreamSupport.stream(spliterator, false)
        return new ReferencePipeline.Head(spliterator, StreamOpFlag.fromCharacteristics(spliterator), false)
Predicate fishPredicate = /* new lambda f -> f.contains("fish") */
Stream stream2 = stream1.filter(fishPredicate);
    return new StatelessOp(this, StreamShape.REFERENCE, StreamOpFlag.NOT_SIZED) { /* ... */ }
Function fishFunction = /* new lambda f.substring(0, 1).toUpperCase() + f.substring(1) */
Stream stream3 = stream2.map(fishFunction);
    return new StatelessOp(this, StreamShape.REFERENCE, StreamOpFlag.NOT_SORTED | StreamOpFlag.NOT_DISTINCT) { /* ... */ }
Collector collector = Collectors.toList();
    Supplier supplier = /* new lambda */
    BiConsumer accumulator = /* new lambda */
    BinaryOperator combiner = /* new lambda */
    return new CollectorImpl<>(supplier, accumulator, combiner, CH_ID);
List hasFishList = stream3.collect(collector)
    // ReferencePipeline#StatelessOp#collect(Collector):
    List container;
    if (stream3.isParallel() && /* not executed */) { /* not executed */ }
    else {
    /*>*/TerminalOp terminalOp = ReduceOps.makeRef(collector)
            Supplier supplier = Objects.requireNonNull(collector).supplier();
            BiConsumer accumulator = collector.accumulator();
            BinaryOperator combiner = collector.combiner();
            return new ReduceOp(StreamShape.REFERENCE) { /* ... */ }
    /*>*/container = stream3.evaluate(terminalOp);
            // AbstractPipeline#evaluate(TerminalOp):
            if (linkedOrConsumed) { /* not executed */ }
            linkedOrConsumed = true;
            if (isParallel()) { /* not executed */ }
            else {
            /*>*/Spliterator spliterator2 = sourceSpliterator(terminalOp.getOpFlags())
                    // AbstractPipeline#sourceSpliterator(int):
                    if (sourceStage.sourceSpliterator != null) { /* not executed */ }
                    /* ... */
                    if (isParallel()) { /* not executed */ }
                    return spliterator;
            /*>*/terminalOp.evaluateSequential(stream3, spliterator2);
                    // ReduceOps#ReduceOp#evaluateSequential(PipelineHelper, Spliterator):
                    ReducingSink sink = terminalOp.makeSink()
                        return new ReducingSink()
                    Sink sink = terminalOp.wrapAndCopyInto(sink, spliterator)
                        Sink wrappedSink = wrapSink(sink)
                            // AbstractPipeline#wrapSink(Sink)
                            for (/* executed twice */) { p.opWrapSink(p.previousStage.combinedFlags, sink) }
                                return new Sink.ChainedReference(sink)
                        terminalOp.copyInto(wrappedSink, spliterator);
                            // AbstractPipeline#copyInto()
                            if (!StreamOpFlag.SHORT_CIRCUIT.isKnown(getStreamAndOpFlags())) {
                            /*>*/wrappedSink.begin(spliterator.getExactSizeIfKnown());
                            /*>*/ /* not important */
                            /*>*/supplier.get() // initializes ArrayList
                            /*>*/spliterator.forEachRemaining(wrappedSink)
                                    // Spliterators#ArraySpliterator#foreachRemaining(Consumer):
                                    // ... unimportant code
!!                                  do {
                                    /*>*/action.accept((String)a[i])
                                    } while (++i < hi) // for each fish :)
                            /*>*/wrappedSink.end() // no-op
                            } else { /* not executed */}
                        return sink;
                    return sink.get()
            }
    /*>*/if (collector.characteristics().contains(Collector.Characteristics.IDENTITY_FINISH)) { return container; }
    /*>*/else { /* not executed */ }
Run Code Online (Sandbox Code Playgroud)

在惊叹号指向实际主力:一个do-while循环fishListSpliterator.这里是do-while循环的更详细的跟踪:

do {
/*>*/action.accept((String)a[i])
    if (predicate.test(u)) { downstream.accept(u); }  // predicate is our fishPredicate
        downstream.accept(mapper.apply(u)); // mapper is our fishFunction
            accumulator.accept(u)
                // calls add(u) on resulting ArrayList
} while (++i < hi) // for each fish :)
Run Code Online (Sandbox Code Playgroud)


在字节码级别上使用Lambdas的Streams API

让我们看一下执行代码的相关部分在字节码中的外观.有趣的是如何

fishList.stream().filter(f -> f.contains("fish")).map(f -> f.substring(0, 1).toUpperCase() + f.ubstring(1)).collect(Collectors.toList());
Run Code Online (Sandbox Code Playgroud)

已翻译.您可以在pastebin上找到完整版本.我将只关注
filter(f -> f.contains("fish"))这里:

invokedynamic #26,  0         // InvokeDynamic #0:test:()Ljava/util/function/Predicate; [
    java/lang/invoke/LambdaMetafactory.metafactory(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;
    (Ljava/lang/Object;)Z, 
    FishTest.lambda$fish8$0(Ljava/lang/String;)Z, 
    (Ljava/lang/String;)Z
  ]
invokeinterface #27,  2       // InterfaceMethod java/util/stream/Stream.filter:(Ljava/util/function/Predicate;)Ljava/util/stream/Stream;
Run Code Online (Sandbox Code Playgroud)

这里没有特定于流API的内容,但invokedynamic指令用于创建lambdas.Java 7相当于lambdas将创建匿名内部类实现Predicate.这将被转换为字节码:

new FishTest$1                        // create new instance of Predicate
dup
invokespecial FishTest$1.<init>()V    // call constructor
Run Code Online (Sandbox Code Playgroud)

在Java 8中创建lambda将被翻译为单个invokedynamic指令,而无需创建新对象.invokedynamic指令的目的是将lambda的创建推迟到运行时(而不是编译时).这样可以实现缓存lambda实例等功能:

使用invokedynamic可以让我们将转换策略的选择推迟到运行时.运行时实现可以自由选择策略来评估lambda表达式.... invokedynamic机制允许这样做,而没有这种后期绑定方法可能带来的性能成本....例如,...我们在第一次调用给定的lambda工厂站点时生成类.此后,将来对该lambda工厂站点的调用将重新使用第一次调用时生成的类.

invokedynamic给出用于构造相应功能接口的实例的"配方"的参数.它们表示运行时实例创建的元数据,对其实现的方法的引用(即Predicate.test())和方法的实现.
在我们的例子中,实现是调用静态方法boolean lambda$fish8$0(String),编译器潜入我们的类.它包含实际的字节码f.contains("fish").如果您使用lambda捕获方法引用(例如list::add),从外部范围捕获的变量等,事情会变得更复杂 - 在本文档中查找"indy"的出现以获取更多信息.

字节码的其他部分不太有趣.除了明显的循环之外,do-while循环包含一个invokeinterface调用accept()各自的指令Consumer. accept()呼叫沿着水槽传播,沿途呼叫我们的lambdas.这里没什么特别的, lambda调用和通过接收器的invokeinterface传播都是简单的指令.


如何读取伪代码

缩进用于在缩进代码上方显示未展开的调用体.代码开始/*>*/表示当前调用的继续(当需要更好的可读性时).所以打电话

Objects.requireNonNull(new Object());
Run Code Online (Sandbox Code Playgroud)

将以跟踪伪代码写为:

Object o = new Object(); // extracted variable to improve visibility of new instance creation
Objects.requireNonNull(o);
    // this is the body of Objects.requireNonNull():
    if (o == null) {
    /*>*/throw new NullPointerException(); // this line is still part of  requireNonNull() body
    }
    return o;
Run Code Online (Sandbox Code Playgroud)

我还跳过了一些不重要的调用,如空检查,省略了通用参数,在适当的情况下将变量内联表达式提取到变量等,以提高可读性.