gpt4 book ai didi

java - 在字节码级别理解Java 8流

转载 作者:搜寻专家 更新时间:2023-10-30 21:45:51 25 4
gpt4 key购买 nike

在线上有大量有关Java 8中流的信息和教程。我发现的大多数内容都很好地解释了流的各种元素在概念级别上是如何工作的。但是,我还没有遇到很多描述JVM在后台实际实现和执行流的 Material 。

考虑在使用流和使用Java 8之前的传统方法之间比较Collection上的操作。两种方法之间的底层字节码看起来是否相同?性能会一样吗?

为了更具体,请考虑以下示例,在该示例中,我需要找到所有名称中包含“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());

您可能对字节码级别上的这两种方法有何不同有任何见解。而且一些实际的字节码会更好。

最佳答案

随着时间的流逝,答案已经增长了很多,因此我将以一个摘要开始:
观察结果

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

  • 在示例中通过执行流跟踪执行
    下面的伪代码使用流通过执行版本来捕获跟踪。请参阅这篇文章的底部以获取有关如何读取跟踪的说明。
    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 */ }
    感叹号指向 实际的主力:fishListSpliterator中的do-while循环。这是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 :)

    在字节码级别具有Lambda的Streams API
    让我们看一下所执行的代码的相关部分在字节码中的样子。有趣的是
    fishList.stream().filter(f -> f.contains("fish")).map(f -> f.substring(0, 1).toUpperCase() + f.ubstring(1)).collect(Collectors.toList());
    已翻译。您可以找到完整版本 on 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;

    那里没有 特定于流API的,但是新的 指令用于创建lambda。与Java 7中的lambda等效的方法是创建实现invokedynamic的匿名内部类。这将被翻译为字节码:
    new FishTest$1                        // create new instance of Predicate
    dup
    invokespecial FishTest$1.<init>()V // call constructor
    相反,在Java 8中创建lambda会转换为单个Predicate指令,而无需创建新对象。 invokedynamic指令的目的是将lambda的创建推迟到运行时(与编译时相反)。这将启用caching lambda instances之类的功能:

    The use of invokedynamic lets us defer the selection of a translation strategy until run time. The runtime implementation is free to select a strategy dynamically to evaluate the lambda expression. ... The invokedynamic mechanics allow this to be done without the performance costs that this late binding approach might otherwise impose. ... For example, ... we generate the class the first time a given lambda factory site is called. Thereafter, future calls to that lambda factory site will re-use the class generated on the first call.

    invokedynamic的参数为构造相应功能接口(interface)的实例提供了“配方”。它们代表用于创建运行时实例的元工厂,引用其实现的方法(即invokedynamic)和该方法的实现。
    在我们的例子中,实现是静态方法 Predicate.test() 的调用,编译器潜入了该方法。它包含boolean lambda$fish8$0(String)的实际字节码。如果您使用lambda捕获方法引用(例如f.contains("fish")),从外部范围捕获的变量等,则情况会变得更加复杂-在this document中查找“indy”的出现以获取更多信息。
    The other parts of bytecode不太有趣。除了明显的循环外,do-while循环还包含一个list::add指令,该指令在相应的invokeinterface上调用accept()Consumer调用沿接收器传播,并在此过程中调用我们的lambda。在这里没什么特别的,和lambda都调用,通过接收器传播都是简单的accept()指令

    如何读取伪代码
    缩进用于在缩进代码上方显示展开的调用主体。以invokeinterface开头的代码表示当前调用的继续(当需要更好的可读性时)。因此打电话
    Objects.requireNonNull(new Object());
    将在跟踪伪代码中写为:
    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;
    我还跳过了一些不重要的调用,例如空检查,省略了通用参数,在适当的情况下将内联表达式提取为变量等,以提高可读性。

    关于java - 在字节码级别理解Java 8流,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/32166193/

    25 4 0
    Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
    广告合作:1813099741@qq.com 6ren.com