解封箱会减慢Java流吗?

huangapple go评论61阅读模式
英文:

Does unboxing slow down Java streams?

问题

我有以下的类:

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);
    }
}

这个类有两个方法,接受一个整数列表并返回最小的数字。其中一种方法是将Integer的compareTo()方法作为比较器传递给min()函数。另一种方法是从列表中获取一个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
英文:

I have the following class:

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) + &quot; seconds!&quot;);
    }

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

    private Integer minNoUnboxing(List&lt;Integer&gt; list) {
        return list.stream().min(Integer::compareTo).orElse(-1);
    }

    private Integer minWithUnboxing(List&lt;Integer&gt; list) {
        return list.stream().mapToInt(x -&gt; x).min().orElse(-1);
    }
}

This class has 2 methods that take a list of integers and return the smallest number. One way to do it is to pass Integer's compareTo() method as a comparator in the min() function. The other way to do it is to get an IntStream from the list and call the min() function on that.

The second way uses unboxing to map the wrapped ints. Unboxing is famous for being slow when used frequently, but I could not see the difference between using and not using it in this program.

Which way is faster? Or maybe they are both the same?

Thanks.

EDIT:

I took Code-Apprentice's advice and did a bunch of measurements using this approach:

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

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

And it turns out that unboxing is actually 2x faster than the first way. Why is that?

Output:

4.166 no unboxing seconds
1.922 with unboxing seconds

答案1

得分: 4

Unboxing不过是读取Integer对象的int字段的值。这不会减慢操作,因为与另一变体中的Integer实例进行比较时,这些字段也必须被读取。

因此,这些操作在不同的抽象层上工作。

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

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

基本上,这些操作等同于

private Optional&lt;Integer&gt; minNoUnboxing(List&lt;Integer&gt; list) {
    Comparator&lt;Integer&gt; 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) &gt; 0) o = next;
    }
    return Optional.of(o);
}

private OptionalInt minWithUnboxing(List&lt;Integer&gt; list) {
    ToIntFunction&lt;Integer&gt; toInt = x -&gt; 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 &gt; nextInt) i = nextInt;
    }
    return OptionalInt.of(i);
}

除非运行时优化器消除所有差异,我预计对于较大的列表,非装箱版本比较快,因为非装箱在每个元素中仅提取一次int字段,而compareTo必须为每个比较提取两个int值。

英文:

Unboxing is nothing more than reading the value of the int field of the Integer object. This can’t slow down the operation, as for comparing to Integer instances in the other variant, these fields have to be read too.

So, these operations work on different abstractions.

When you use mapToInt(x -&gt; x), you are using a ToIntFunction to tell the implementation how to get int values, then, the min operation works directly on int values.

When you use min(Integer::compareTo), you are using a Comparator to tell the generic implementation, which object is smaller than the other.

Basically, those operation are equivalent to

private Optional&lt;Integer&gt; minNoUnboxing(List&lt;Integer&gt; list) {
    Comparator&lt;Integer&gt; 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) &gt; 0) o = next;
    }
    return Optional.of(o);
}

private OptionalInt minWithUnboxing(List&lt;Integer&gt; list) {
    ToIntFunction&lt;Integer&gt; toInt = x -&gt; 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 &gt; nextInt) i = nextInt;
    }
    return OptionalInt.of(i);
}

Unless the runtime optimizer eliminates all differences, I’d expect the unboxing version to be faster for larger lists, as the unboxing extracts the int field once for each element, whereas the compareTo has to extract two int values for each comparison.

答案2

得分: 1

性能影响几乎与拆箱无关,而与你正在比较的两种基本不同的操作有关(使用比较器进行最小化与使用约简操作)。

请参阅这些基准测试:

@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

另外请注意,与装箱相比,拆箱非常快速。在最坏的情况下,拆箱只是字段访问/指针解引用,而装箱可能涉及对象实例化。

英文:

The performance impact has almost literally nothing to do with unboxing and everything to do with the fact that you're comparing two fundamentally different operations (minimizing with comparator vs. reduction)

See these benchmarks:

@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) -&gt; a &lt; b ? a : b).orElse(-1);
}

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

Results:

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

Edit: Also note that unboxing is VERY FAST compared to boxing. Unboxing is simply a field access/pointer dereference in the worst case whereas boxing can involve object instantiation.

huangapple
  • 本文由 发表于 2020年9月29日 23:29:48
  • 转载请务必保留本文链接:https://go.coder-hub.com/64122809.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定