Reputation: 352
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
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
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