博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
Java 自动装箱性能
阅读量:2117 次
发布时间:2019-04-30

本文共 6761 字,大约阅读时间需要 22 分钟。

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上访问。

原文链接: 
 翻译: 

译文链接: 

转载地址:http://dfref.baihongyu.com/

你可能感兴趣的文章
Linux下perf性能测试火焰图只显示函数地址不显示函数名的问题
查看>>
c结构体、c++结构体和c++类的区别以及错误纠正
查看>>
Linux下查看根目录各文件内存占用情况
查看>>
A星算法详解(个人认为最详细,最通俗易懂的一个版本)
查看>>
利用栈实现DFS
查看>>
逆序对的数量(递归+归并思想)
查看>>
数的范围(二分查找上下界)
查看>>
算法导论阅读顺序
查看>>
Windows程序设计:直线绘制
查看>>
linux之CentOS下文件解压方式
查看>>
Django字段的创建并连接MYSQL
查看>>
div标签布局的使用
查看>>
HTML中表格的使用
查看>>
(模板 重要)Tarjan算法解决LCA问题(PAT 1151 LCA in a Binary Tree)
查看>>
(PAT 1154) Vertex Coloring (图的广度优先遍历)
查看>>
(PAT 1115) Counting Nodes in a BST (二叉查找树-统计指定层元素个数)
查看>>
(PAT 1143) Lowest Common Ancestor (二叉查找树的LCA)
查看>>
(PAT 1061) Dating (字符串处理)
查看>>
(PAT 1118) Birds in Forest (并查集)
查看>>
数据结构 拓扑排序
查看>>