java自动装箱性能

news/2024/7/2 23:56:32

2019独角兽企业重金招聘Python工程师标准>>> hot3.png

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

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

编译器自动将它转换为

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

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

实例

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

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 rowsList 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类型:

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),它可以帮我们解决大部分难题。如果感兴趣的话,推荐大家阅读文档和实例;它会很吸引你。以下是基准测试:

@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就可以做一些操作让系统认为返回值会被使用到,防止冗余代码消除影响了结果。)

以下是结果:

$ 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/opResult "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:16Benchmark 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 命令,就可以看到结果:

$ 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()的取值来源:

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 一样的内容。

解决方法

解决方法很简单:

@ @ -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就很容易理解代码:

private final char value;public Character(char value) {
this.value = value;
}public char charValue() {
return value;
}

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

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

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

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


转载于:https://my.oschina.net/u/734885/blog/628569


http://lihuaxi.xjx100.cn/news/285826.html

相关文章

[NC16591]关押罪犯 并查集

题解&#xff1a;很明显的并查集&#xff0c;但因为它们带有权值&#xff0c;所以我们先要把他排序&#xff0c;我们要尽可能让危害大的罪犯在两个监狱里&#xff08;这里有一点贪心的味道&#xff09;。 1.首先我们把它门按照之间的影响值从大到小排序。 2.假设a与b是敌人&…

简单分析MySQL 一则慢日志监控误报问题

这篇文章主要介绍了MySQL 一则慢日志监控误报的问题分析与解决&#xff0c;帮助大家更好的理解和使用MySQL&#xff0c;感兴趣的朋友可以了解下 之前因为各种原因&#xff0c;有些报警没有引起重视&#xff0c;最近放假马上排除了一些潜在的人为原因&#xff0c;发现数据库的慢…

一直在努力的你是否也是这样的(经典语录总结)

1.下次再遇见喜欢的人&#xff0c;一定要提醒自己&#xff0c;只做朋友&#xff0c;只谈笑风生&#xff0c;不可以动情&#xff0c;不远不近的欣赏&#xff0c;淡淡的喜欢&#xff0c;不至于最后乱了初心&#xff0c;败了芳华。 ——杨绛 2.走好选择的路&#xff0c;别选择好走…

前缀和 + ST表 ---- CF 1556 E. Equilibrium(两个序列 + - 操作使得每位相等) 详解

题目连接 题目大意&#xff1a; 就是给你两个长度为nnn的a,ba,ba,b数组&#xff0c;给你q∈[1,1e5]q\in[1,1e5]q∈[1,1e5]次询问&#xff0c;每次询问一个区间[l,r][l,r][l,r] 你对这个区间里面的数可以进行一下操作 取出偶数个位置 l≤pos1<pos2<....<posz≤r∣[z%…

手写 30 个主流机器学习算法,代码超 3 万行,全都开源了!

点击上方“视学算法”&#xff0c;选择“星标”公众号第一时间获取价值内容本文经机器之心&#xff08;ID&#xff1a;almosthuman2014&#xff09;授权转载&#xff0c;禁二次转载参与&#xff1a;思源、一鸣、张倩用 NumPy 手写所有主流 ML 模型&#xff0c;普林斯顿博士后 D…

赵本山:我的时代还没有结束 | Python告诉你

作者 | 丁彦军来源 | 恋习Python&#xff08;ID: sldata2017&#xff09;【AI科技大本营按】今年春晚的小品好看吗&#xff1f;没有了赵本山的春晚总觉得少了点什么&#xff0c;然而许久不登春晚舞台的本山大叔借着B站的东风证明了「你大爷还是你大爷」。最近很多人被“改革春…

RSS 语法概述

RSS 指 Really Simple Syndication&#xff08;真正简易联合&#xff09;&#xff0c;RSS 2.0 的语法很简单&#xff0c;也很严格。RSS 用于在网站间分享信息。RSS 语法 RSS 2.0 的语法很简单&#xff0c;也很严格。 RSS 如何工作 RSS 用于在网站间分享信息。 使用 RSS&…

[NOI2015]程序自动分析

题解&#xff1a;并查集&#xff0c;因为数据比较大嘛&#xff0c;你要是想再所以如果开1e9的数组大小&#xff0c;那毫无疑问&#xff0c;基佬紫等着你。 所以这个题离散化用了map这个容器。 首先&#xff0c;遍历一遍e1的时候&#xff0c;把他们之间相等的都给连到同一个集合…