从 Java 函数有效地返回两个值

Returning two values from Java function efficiently

有人知道是否有办法 return 来自 Java 的两个值且开销(接近)零吗?我只是在寻找两个值——我有几个用例,从处理字节数组(需要 return 值和下一个起始位置)到尝试 return 一个有错误的值用定点计算做一些丑陋的代码,需要整数和小数部分。

我不在乎一些非常丑陋的技巧。该函数很小,Hotspot 很乐意将其内联。所以现在,我只需要让 Hotspot 基本上消除任何对象创建或位移。

如果我将我的 returned 值限制为整数,我会尝试将它们打包成一个 long,但即使在内联之后,Hotspot 似乎也无法弄清楚所有的位移位和掩码都没有真的做任何事情,它愉快地将整数打包和解包成相同的值(显然,这是 Hotspot 的窥孔优化器需要帮助的地方)。但至少我没有创建对象。

我更困难的情况是当我需要 return 的项目之一是引用而另一个是长引用或另一个引用时(对于 int 情况,我想我可以压缩 OOP 并使用上面描述的位封装)。

有没有人试图让 Hotspot 为此生成无垃圾代码?现在最糟糕的情况是我必须随身携带一个物体并将其传递进去,但我想保持它独立。 Thread Locals 开销很大(哈希查找),需要可重入。

您描述的解决方案与您在 Hotspot 中获得的解决方案几乎一样好——传递一个对象来保存 return 值并改变它。 (Java 10 种值类型在这里可能做得更好,但我认为这甚至还未处于原型阶段。)

就是说:短寿命的小对象实际上距离零开销并不远。寿命短的对象的垃圾收集故意非常便宜。

我不得不处理这个问题,发现最好的方法是用 public 字段实例化一个简单的 final class,然后将它作为参数传递给您的方法。将结果推送到该实例。

如果您有循环,请尝试尽可能长时间地重复使用该实例。

在 Java 7 中(当我这样做时)具有 setter 和 getter 的开销非常小。每个循环实例化新对象也是如此。

-XX:+EliminateAllocations 优化(在 Java 8 中默认开启)对此效果很好。

每当您 return new Pair(a, b) 就在被调用方方法的末尾并立即在调用方中使用结果时,如果被调用方是内联的,JVM 很可能会进行标量替换。

一个简单的实验表明 return 对象几乎没有开销。这不仅是一种高效的方式,也是最易读的方式。

Benchmark                        Mode  Cnt    Score   Error   Units
ReturnPair.manualInline         thrpt   30  127,713 ± 3,408  ops/us
ReturnPair.packToLong           thrpt   30  113,606 ± 1,807  ops/us
ReturnPair.pairObject           thrpt   30  126,881 ± 0,478  ops/us
ReturnPair.pairObjectAllocated  thrpt   30   92,477 ± 0,621  ops/us

基准:

import org.openjdk.jmh.annotations.*;
import org.openjdk.jmh.infra.Blackhole;

import java.util.concurrent.ThreadLocalRandom;

@State(Scope.Benchmark)
public class ReturnPair {
    int counter;

    @Benchmark
    public void manualInline(Blackhole bh) {
        bh.consume(counter++);
        bh.consume(ThreadLocalRandom.current().nextInt());
    }

    @Benchmark
    public void packToLong(Blackhole bh) {
        long packed = getPacked();
        bh.consume((int) (packed >>> 32));
        bh.consume((int) packed);
    }

    @Benchmark
    public void pairObject(Blackhole bh) {
        Pair pair = getPair();
        bh.consume(pair.a);
        bh.consume(pair.b);
    }

    @Benchmark
    @Fork(jvmArgs = "-XX:-EliminateAllocations")
    public void pairObjectAllocated(Blackhole bh) {
        Pair pair = getPair();
        bh.consume(pair.a);
        bh.consume(pair.b);
    }

    public long getPacked() {
        int a = counter++;
        int b = ThreadLocalRandom.current().nextInt();
        return (long) a << 32 | (b & 0xffffffffL);
    }

    public Pair getPair() {
        int a = counter++;
        int b = ThreadLocalRandom.current().nextInt();
        return new Pair(a, b);
    }

    static class Pair {
        final int a;
        final int b;

        Pair(int a, int b) {
            this.a = a;
            this.b = b;
        }
    }
}