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
Java 自动装箱性能相关推荐
- java自动装箱性能
2019独角兽企业重金招聘Python工程师标准>>> Java 的基本数据类型(int.double. char)都不是对象.但由于很多Java代码需要处理的是对象(Object) ...
- java 自动装箱自动拆箱,java自动装箱、自动拆箱和正常情况性能比较
自动装箱和自动拆箱是java5.0版本引入的,能自动将基本类型转换为对应的基本类型包装对象,那么我们比较一下他们的性能情况. package com.wmmad.test; import junit. ...
- Java自动拆装箱面试_跟王老师学泛型(二):Java自动装箱与拆箱
Java 自动装箱与拆箱(Autoboxing and unboxing) 主讲教师:王少华 QQ群:483773664 学习目标: 掌握Java 基本数据对应的包装类 掌握Java 自动装箱与拆箱 ...
- 【转】java 自动装箱与拆箱
java 自动装箱与拆箱 这个是jdk1.5以后才引入的新的内容,作为秉承发表是最好的记忆,毅然决定还是用一篇博客来代替我的记忆: java语言规范中说道:在许多情况下包装与解包装是由编译器自行完成的 ...
- Java 自动装箱与拆箱
Java 自动装箱与拆箱 装箱就是自动将基本数据类型转换为包装器类型(int–>Integer):调用方法:Integer 的 valueOf(int) 方法 拆箱就是自动将包装器类型转换为基本 ...
- java 自动装箱自动拆箱
1.Java数据类型 在介绍Java的自动装箱和拆箱之前,我们先来了解一下Java的基本数据类型. 在Java中,数据类型可以分为两大种,Primitive Type(基本类型)和Reference ...
- java自动装箱拆箱原理
java自动装箱拆箱原理 看了很多博主都没写原理,只是浅显地说了自动装箱拆箱的含义,我就把这个必须知道的知识写一下吧 1.自动装箱 以int -> Integer为例,Integer integ ...
- java自动装箱和自动拆箱
Java自动装箱和自动拆箱是Java语言的一颗语法糖. 下面说一下java的自动装箱和拆箱的例子 1. 深入理解java虚拟机上的一个例子 public static void testJvmBook ...
- java的自动装箱_详解Java 自动装箱与拆箱的实现原理
详解Java 自动装箱与拆箱的实现原理 发布于 2020-7-4| 复制链接 本篇文章主要介绍了详解Java 自动装箱与拆箱的实现原理,小妖觉得挺不错的,现在分享给大家,也给大家做个参考.一起跟随小妖 ...
最新文章
- Generator-ing Values
- html 访问 php 变量
- python只能使用内置数据库_隐藏彩蛋:你知道python有一个内置的数据库吗?
- 在Win7中怎样打开摄像头?
- 关键字restrict简介
- java 格式化日期(DateFormat)
- ES6——函数参数默认值
- dump排查内存异常 java_如何排查Java应用内存泄漏问题
- matlab求解集合覆盖问题,Set Cover Problem (集合覆盖问题)
- 【UWA 学堂】部分渲染课程的调价通知
- 拍一拍身上的土(2009-12-12 01:17:43韩寒博客)
- 2018年第九届蓝桥杯真题C/C++B组
- AI智能语音系统源码搭建
- 基于spring cloud + nacos + gateway + ssm+的学生管理系统
- 计算机 高级软件工程师考试试题,高级软件工程师8月考试试题A卷带答案.doc
- 灰度测试或 AB 测试
- tp-link与台式计算机连接教程,台式电脑连接到无线路由器上网教程
- 零基础爬取堆糖网图片(一)
- MyBatis-Plus DQL与其他知识点
- 实验十三:PCF8591模数传感器-数模转换实验
热门文章
- 支付系统路由系统设计
- 独家 | 大数据与AI技术在金融科技的应用
- oracle19c怎么创建Scott,Oracle db-sample-schema-19c安装(scott hr oe pm ix sh bi用户创建部署)...
- MySQL - 无索引行锁升级为表锁
- RocketMQ-初体验RocketMQ(06)-使用API操作RocketMQ ,理解RocketMQ的存储结构
- java gc种类_Java GC系列(3):垃圾回收器种类
- beautifulsoup解析动态页面div未展开_两个资讯爬虫解析库的用法与对比
- Java中的nextInt()和next()与nextLine()区别详解
- 坡道行驶电动小车_事发红绿灯路口!东莞一女子骑电动滑板车被撞致颅内出血…...
- 计算机科学与技术的深度研究,专业深度分析--计算机科学与技术.docx