Ivan Dimitrov
Ivan Dimitrov

Reputation: 352

Does unboxing slow down Java streams?

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

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

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

Upvotes: 0

Views: 589

Answers (2)

Rubydesic
Rubydesic

Reputation: 3476

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

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

Results:

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

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.

Upvotes: 1

Holger
Holger

Reputation: 298143

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

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.

Upvotes: 4

Related Questions