Java 自动装箱性能

Java 的基本数据类型(int、double、 char)都不是对象。但由于很多Java代码需要处理的是对象(Object),Java给所有基本类型提供了包装类(Integer、Double、Character)。有了自动装箱,你可以写如下的代码

1
2
Character boxed = 'a' ;
char unboxed = boxed;

编译器自动将它转换为

1
2
Character boxed = Character.valueOf( 'a' );
char unboxed = boxed.charValue();

然而,Java虚拟机不是每次都能理解这类过程,因此要想得到好的系统性能,避免不必要的装箱很关键。这也是 OptionalInt 和 IntStream 等特殊类型存在的原因。在这篇文章中,我将概述JVM很难消除自动装箱的一个原因。

实例

例如,我们想要计算任意一类数据的编辑距离(Levenshtein距离),只要这些数据可以被看作一个序列:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
public class Levenshtein{
private final Function> asList;
 
public Levenshtein(Function> asList) {
this .asList = asList;
}
 
public int distance(T a, T b) {
// Wagner-Fischer algorithm, with two active rows
 
List aList = asList.apply(a);
List bList = asList.apply(b);
 
int bSize = bList.size();
int [] row0 = new int [bSize + 1 ];
int [] row1 = new int [bSize + 1 ];
 
for ( int i = 0 ; i row0[i] = i;
}
 
for ( int i = 0 ; i < bSize; ++i) {
U ua = aList.get(i);
row1[ 0 ] = row0[ 0 ] + 1 ;
 
for ( int j = 0 ; j < bSize; ++j) {
U ub = bList.get(j);
int subCost = row0[j] + (ua.equals(ub) ? 0 : 1 );
int delCost = row0[j + 1 ] + 1 ;
int insCost = row1[j] + 1 ;
row1[j + 1 ] = Math.min(subCost, Math.min(delCost, insCost));
}
 
int [] temp = row0;
row0 = row1;
row1 = temp;
}
 
return row0[bSize];
}
}

只要两个对象可以被看作List,这个类就可以计算它们的编辑距离。如果想计算String类型的距离,那么就需要把String转变为List类型:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class StringAsList extends AbstractList{
private final String str;
 
public StringAsList(String str) {
this .str = str;
}
 
@Override
public Character get( int index) {
return str.charAt(index); // Autoboxing! }
 
@Override
public int size() {
return str.length();
}
}
 
...
 
Levenshteinlev = new Levenshtein<>(StringAsList:: new );
lev.distance( "autoboxing is fast" , "autoboxing is slow" ); // 4

由于Java泛型的实现方式,不能有List类型,所以要提供List和装箱操作。(注:Java10中,这个限制也许会被取消。)

基准测试

为了测试 distance() 方法的性能,需要做基准测试。Java中微基准测试很难保证准确,但幸好OpenJDK提供了JMH(Java Microbenchmark Harness),它可以帮我们解决大部分难题。如果感兴趣的话,推荐大家阅读文档和实例;它会很吸引你。以下是基准测试:

1
2
3
4
5
6
7
8
9
10
11
@State (Scope.Benchmark)
public class MyBenchmark {
private Levenshtein lev = new Levenshtein<>(StringAsList:: new );
 
@Benchmark
@BenchmarkMode (Mode.AverageTime)
@OutputTimeUnit (TimeUnit.NANOSECONDS)
public int timeLevenshtein() {
return lev.distance( "autoboxing is fast" , "autoboxing is slow" );
}
}

(返回方法的结果,这样JMH就可以做一些操作让系统认为返回值会被使用到,防止冗余代码消除影响了结果。)

以下是结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
$ java -jar target/benchmarks.jar -f 1 -wi 8 -i 8
# JMH 1.10 . 2 (released 3 days ago)
# VM invoker: /usr/lib/jvm/java- 8 -openjdk/jre/bin/java
# VM options:
# Warmup: 8 iterations, 1 s each
# Measurement: 8 iterations, 1 s each
# Timeout: 10 min per iteration
# Threads: 1 thread, will synchronize iterations
# Benchmark mode: Average time, time/op
# Benchmark: com.tavianator.boxperf.MyBenchmark.timeLevenshtein
 
# Run progress: 0.00 % complete, ETA 00 : 00 : 16
# Fork: 1 of 1
# Warmup Iteration 1 : 1517.495 ns/op
# Warmup Iteration 2 : 1503.096 ns/op
# Warmup Iteration 3 : 1402.069 ns/op
# Warmup Iteration 4 : 1480.584 ns/op
# Warmup Iteration 5 : 1385.345 ns/op
# Warmup Iteration 6 : 1474.657 ns/op
# Warmup Iteration 7 : 1436.749 ns/op
# Warmup Iteration 8 : 1463.526 ns/op
Iteration 1 : 1446.033 ns/op
Iteration 2 : 1420.199 ns/op
Iteration 3 : 1383.017 ns/op
Iteration 4 : 1443.775 ns/op
Iteration 5 : 1393.142 ns/op
Iteration 6 : 1393.313 ns/op
Iteration 7 : 1459.974 ns/op
Iteration 8 : 1456.233 ns/op
 
Result "timeLevenshtein" :
1424.461 ±( 99.9 %) 59.574 ns/op [Average]
(min, avg, max) = ( 1383.017 , 1424.461 , 1459.974 ), stdev = 31.158
CI ( 99.9 %): [ 1364.887 , 1484.034 ] (assumes normal distribution)
 
# Run complete. Total time: 00 : 00 : 16
 
Benchmark Mode Cnt Score Error Units
MyBenchmark.timeLevenshtein avgt 8 1424.461 ± 59.574 ns/op

分析

为了查看代码热路径(hot path)上的结果,JMH集成了Linux工具perf,可以查看最热代码块的JIT编译结果。(要想查看汇编代码,需要安装hsdis插件。我在AUR上提供了下载,Arch用户可以直接获取。)在JMH命令行添加 -prof perfasm 命令,就可以看到结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$ java -jar target/benchmarks.jar -f 1 -wi 8 -i 8 -prof perfasm
...
cmp $ 0x7f ,%eax
jg 0x00007fde989a6148 ;*if_icmpgt
; - java.lang.Character::valueOf @3 (line 4570 )
; - com.tavianator.boxperf.StringAsList::get @8 (line 14 )
; - com.tavianator.boxperf.StringAsList::get @2 ; (line 5 )
; - com.tavianator.boxperf.Levenshtein::distance @121 (line 32 )
cmp $ 0x80 ,%eax
jae 0x00007fde989a6103 ;*aaload
; - java.lang.Character::valueOf @ 10 (line 4571 )
; - com.tavianator.boxperf.StringAsList::get @8 (line 14 )
; - com.tavianator.boxperf.StringAsList::get @ 2 (line 5 )
; - com.tavianator.boxperf.Levenshtein::distance @121 (line 32 )
...

输出内容很多,但上面的一点内容就说明装箱没有被优化。为什么要和0x7f/0×80的内容做比较呢?原因在于Character.valueOf()的取值来源:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
private static class CharacterCache {
private CharacterCache(){}
 
static final Character cache[] = new Character[ 127 + 1 ];
 
static {
for ( int i = 0 ; i < cache.length; i++)
cache[i] = new Character(( char )i);
}
}
 
public static Character valueOf( char c) {
if (c return CharacterCache.cache[( int )c];
}
return new Character(c);
}

可以看出,Java语法标准规定前127个char的Character对象放在缓冲池中,Character.valueOf()的结果在其中时,直接返回缓冲池的对象。这样做的目的是减少内存分配和垃圾回收,但在我看来这是过早的优化。而且它妨碍了其他优化。JVM无法确定 Character.valueOf(c).charValue() == c,因为它不知道缓冲池的内容。所以JVM从缓冲池中取了一个Character对象并读取它的值,结果得到的就是和 c 一样的内容。

解决方法

解决方法很简单:

1
2
3
4
5
6
7
8
9
@ @ - 11 , 7 + 11 , 7 @ @ public class StringAsList extends AbstractList {
 
@Override
public Character get( int index) {
- return str.charAt(index); // Autoboxing!
+ return new Character(str.charAt(index));
}
 
@Override

用显式的装箱代替自动装箱,就避免了调用Character.valueOf(),这样JVM就很容易理解代码:

1
2
3
4
5
6
7
8
9
private final char value;
 
public Character( char value) {
this .value = value;
}
 
public char charValue() {
return value;
}

虽然代码中加了一个内存分配,但JVM能理解代码的意义,会直接从String中获取char字符。性能提升很明显:

1
2
3
4
5
6
$ java -jar target/benchmarks.jar -f 1 -wi 8 -i 8
...
# Run complete. Total time: 00 : 00 : 16
 
Benchmark Mode Cnt Score Error Units
MyBenchmark.timeLevenshtein avgt 8 1221.151 ± 58.878 ns/op

速度提升了14%。用 -prof perfasm 命令可以显示,改进以后是直接从String中拿到char值并在寄存器中比较的:

1
2
3
4
5
6
7
8
9
movzwl 0x10 (%rsi,%rdx, 2 ),%r11d ;*caload
; - java.lang.String::charAt @27 (line 648 )
; - com.tavianator.boxperf.StringAsList::get @9 (line 14 )
; - com.tavianator.boxperf.StringAsList::get @ 2 (line 5 )
; - com.tavianator.boxperf.Levenshtein::distance @121 (line 32 )
cmp %r11d,%r10d
je 0x00007faa8d404792 ;*if_icmpne
; - java.lang.Character::equals @18 (line 4621 )
; - com.tavianator.boxperf.Levenshtein::distance @137 (line 33 )

总结

装箱是HotSpot的一个弱项,希望它能做到越来越好。它应该多利用装箱类型的语义,消除装箱操作,这样以上的解决办法就没有必要了。

以上的基准测试代码都可以在GitHub上访问。

原文链接:  tavianator  翻译:  ImportNew.com  rainsbaby
译文链接:  http://www.importnew.com/16737.html
转载请保留原文出处、译者和译文链接。 ]
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值