【问题标题】:Does unboxing slow down Java streams?拆箱会减慢 Java 流的速度吗?
【发布时间】:2020-09-29 15:29:48
【问题描述】:

我有以下课程:

public final class App {
    private App() {
    }

    public static void main(String[] args) {
        Stopwatch stopwatch = Stopwatch.createStarted();
        new App().main();
        System.out.println(((double) stopwatch.elapsed(TimeUnit.MICROSECONDS) * 1_000_000) + " seconds!");
    }

    private void main() {
        List<Integer> list = new ArrayList<>();
        for (int i = 0; i < 1000000; i++) {
            list.add(ThreadLocalRandom.current().nextInt(1000));
        }
        System.out.println(minNoUnboxing(list));
        System.out.println(minWithUnboxing(list));
    }

    private Integer minNoUnboxing(List<Integer> list) {
        return list.stream().min(Integer::compareTo).orElse(-1);
    }

    private Integer minWithUnboxing(List<Integer> list) {
        return list.stream().mapToInt(x -> x).min().orElse(-1);
    }
}

这个类有 2 个方法,它们接受一个整数列表并返回最小的数字。一种方法是在 min() 函数中将 Integer 的 compareTo() 方法作为比较器传递。另一种方法是从列表中获取一个 IntStream 并在其上调用 min() 函数。

第二种方法使用拆箱来映射包装的整数。拆箱以频繁使用时速度慢而闻名,但我看不出在这个程序中使用和不使用它的区别。

哪种方式更快?或者它们都一样?

谢谢。

编辑:

我接受了 Code-Apprentice 的建议,并使用这种方法进行了一系列测量:

    Stopwatch noUnboxing = Stopwatch.createStarted();
    for (int i = 0; i < 1000; i++) {
        minNoUnboxing(list);
    }
    System.out.println((double) noUnboxing.elapsed(TimeUnit.MILLISECONDS) / 1000 + " no unboxing seconds");

    Stopwatch withUnboxing = Stopwatch.createStarted();
    for (int i = 0; i < 1000; i++) {
        minWithUnboxing(list);
    }
    System.out.println((double) withUnboxing.elapsed(TimeUnit.MILLISECONDS) / 1000 + " with unboxing seconds");

事实证明,拆箱实际上比第一种方式快 2 倍。这是为什么呢?

输出:

4.166 no unboxing seconds
1.922 with unboxing seconds

【问题讨论】:

  • 找出答案的一种方法是在代码中添加一些时间测量。将每个版本运行一百万次并比较平均值。
  • 您的基准测试方法可能需要重新审视:How do I write a correct micro-benchmark in Java?
  • 试试这个 - How do I write a correct micro-benchmark in Java? 并带着结果回来问一个更有趣的问题。
  • 我做了一些测量,发现拆箱速度更快。为什么会这样?
  • 我总是有一个燃烧期,您在测量之前执行两个循环。这样您就不必处理 JIT 预热时间。在那之后你仍然得到相同的值吗?

标签: java performance java-stream


【解决方案1】:

拆箱无非就是读取Integer对象的int字段的值。这不会减慢操作速度,至于与其他变体中的Integer 实例相比,这些字段也必须读取。

因此,这些操作适用于不同的抽象。

当您使用mapToInt(x -&gt; x) 时,您正在使用ToIntFunction 告诉实现如何获取int 值,然后,min 操作直接作用于int 值。

当您使用min(Integer::compareTo) 时,您使用Comparator 来告诉通用实现,哪个对象比另一个小。

基本上,这些操作等价于

private Optional<Integer> minNoUnboxing(List<Integer> list) {
    Comparator<Integer> c = Integer::compareTo;

    if(list.isEmpty()) return Optional.empty();
    Integer o = list.get(0);
    for(Integer next: list.subList(1, list.size())) {
        if(c.compare(o, next) > 0) o = next;
    }
    return Optional.of(o);
}

private OptionalInt minWithUnboxing(List<Integer> list) {
    ToIntFunction<Integer> toInt = x -> x;

    if(list.isEmpty()) return OptionalInt.empty();
    int i = toInt.applyAsInt(list.get(0));
    for(Integer next: list.subList(1, list.size())) {
        int nextInt = toInt.applyAsInt(next);
        if(i > nextInt) i = nextInt;
    }
    return OptionalInt.of(i);
}

除非运行时优化器消除所有差异,否则我希望拆箱版本对于较大的列表更快,因为拆箱会为每个元素提取一次 int 字段,而 compareTo 必须提取两个 @987654334 @ 每次比较的值。

【讨论】:

  • 这个答案并不正确。拆箱一次和两次几乎没有任何性能影响,速度差异与执行的操作根本不同(最小化 vs min() 实际上是减少)
【解决方案2】:

性能影响几乎与拆箱无关,而与您正在比较两个根本不同的操作(使用比较器最小化与归约)这一事实有关。

查看这些基准:

@Benchmark
public Integer minNoUnboxing(BenchmarkState state) {
    return state.randomNumbers.stream().min(Integer::compareTo).orElse(-1);
}

@Benchmark
public Integer minNoUnboxingReduce(BenchmarkState state) {
    return state.randomNumbers.stream().reduce((a, b) -> a < b ? a : b).orElse(-1);
}

@Benchmark
public Integer minWithUnboxingReduce(BenchmarkState state) {
    return state.randomNumbers.stream().mapToInt(x -> x).min().orElse(-1);
}

结果:

Benchmark                          (listSize)   Mode  Cnt    Score    Error  Units
MyBenchmark.minNoUnboxing             1000000  thrpt    5  128.585 ± 17.617  ops/s
MyBenchmark.minNoUnboxingReduce       1000000  thrpt    5  317.772 ± 27.659  ops/s
MyBenchmark.minWithUnboxingReduce     1000000  thrpt    5  300.348 ± 23.458  ops/s

编辑:还请注意,与装箱相比,拆箱非常快。在最坏的情况下,拆箱只是一个字段访问/指针取消引用,而装箱可能涉及对象实例化。

【讨论】:

  • 您忘了提及这些操作之间的“根本不同”。为其中一个发明一个新名称(“最小化”)是不够的。由于min(Comparator) 在内部代表reduce,我怀疑是否“根本不同”。
  • @Holger 显然它们是不同的,因为尽管“拆箱两次”,minNoUnboxing 和 minNoUnboxingReduce 的性能属性都截然不同。至于为什么会这样,我真的没有心情进行更多分析,但它可能与使用 compareTo 后立即检查结果是否
  • 性能差异不是根本差异的证明(甚至不是标志)。您甚至没有花时间用不同的列表大小进行基准测试来分析操作的扩展方式。由于它们都只是减少,它们都应该线性缩放。但是你没有记录这个基准的任何环境条件,除了列表大小。除此之外,操作并不相同,因为所有 min 变体在相等时保留第一个元素,而您的 (a, b) -&gt; a &lt; b ? a : b 采用最后一个。
猜你喜欢
  • 2014-05-23
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-01-03
  • 1970-01-01
相关资源
最近更新 更多