0

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
Ivan Dimitrov
  • 332
  • 1
  • 10
  • One way to find out is to add some timing measurments to your code. Run each version a million times and compare the average. – Code-Apprentice Sep 29 '20 at 15:31
  • 3
    It's likely that your method of benchmarking is what needs to be revisited: [How do I write a correct micro-benchmark in Java?](https://stackoverflow.com/questions/504103/how-do-i-write-a-correct-micro-benchmark-in-java) – ernest_k Sep 29 '20 at 15:32
  • 3
    Try this out - [How do I write a correct micro-benchmark in Java?](https://stackoverflow.com/questions/504103/how-do-i-write-a-correct-micro-benchmark-in-java?noredirect=1&lq=1) and come back with the results to ask a further interesting question. – Naman Sep 29 '20 at 15:33
  • I did some measurements and it what I found was that unboxing is faster. Why would that be? – Ivan Dimitrov Sep 29 '20 at 15:53
  • I'd always have a burn in period where you execute both loops *before measuring*. That way you don't have to deal with JIT warm up times. Do you still get the same values after that? – Maarten Bodewes Sep 29 '20 at 16:12
  • The accepted answer here isn't correct. This isn't due to JIT weirdness or unboxing but rather due to the fact that one operation is minimizing with compareTo, while the other (though named min()) is actually a reduction using Math.min(). Perform a reduction using the boxed values (see: my answer) and you will see that there is little performance difference. – Rubydesic Sep 29 '20 at 17:38

2 Answers2

4

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.

Holger
  • 285,553
  • 42
  • 434
  • 765
  • 1
    This answer isn't really correct. Unboxing once vs. twice has barely any performance impact, the difference in speed has everything to do with the operations performed being fundamentally different (minimize vs min() which is actually a reduction) – Rubydesic Sep 29 '20 at 17:25
1

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.

Rubydesic
  • 3,386
  • 12
  • 27
  • 3
    You forgot to mention what is so “fundamentally different” between these operations. Inventing a new name (“minimizing”) for one of them is not enough. Since `min(Comparator)` delegates to `reduce` internally, I have my doubt about being “fundamentally different”. – Holger Sep 29 '20 at 19:40
  • @Holger Well evidentally they are different, as both minNoUnboxing and minNoUnboxingReduce have drastically different performance properties despite 'unboxing twice. As to why this is, I'm not really in the mood for profiling more but it probably has to do with all the extra ops from using compareTo followed immediately checking if the result is <= 0. Without any doubt, using min(Integer::compareTo) at any rate is not an apples to apples comparison of boxing and unboxing, which is what I meant by 'fundamantally different'. – Rubydesic Sep 29 '20 at 20:43
  • 2
    A performance difference is not a proof (not even a sign) of a *fundamental* difference. You didn’t even take the time to benchmark with different list sizes to analyze, how the operations scale. Since they all are just reductions, they should all scale linearly. But you didn’t document *any* of the surrounding conditions of this benchmark, except the list size anyway. Besides that, the operations are not identical, as all `min` variants keep the first element when equal whereas your `(a, b) -> a < b ? a : b` takes the last. – Holger Sep 30 '20 at 08:41