英文:
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) + " 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
答案1
得分: 4
Unboxing不过是读取Integer
对象的int
字段的值。这不会减慢操作,因为与另一变体中的Integer
实例进行比较时,这些字段也必须被读取。
因此,这些操作在不同的抽象层上工作。
当您使用mapToInt(x -> 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
必须为每个比较提取两个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 -> 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.
答案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) -> 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.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论