Java 自动装箱性能

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


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

编译器自动将它转换为


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

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

实例

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


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

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


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

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

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

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


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

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

以下是结果:


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

分析

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


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

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


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

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

解决方法

解决方法很简单:


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

@Override

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


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

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


  1. $ java -jar target/benchmarks.jar -f 1 -wi 8 -i 8 
  2. ... 
  3. # Run complete. Total time: 00:00:16 
  4.  
  5. Benchmark Mode Cnt Score Error Units 
  6. 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的一个弱项,希望它能做到越来越好。它应该多利用装箱类型的语义,消除装箱操作,这样以上的解决办法就没有必要了。

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

来源:51CTO

时间: 2024-09-20 01:08:15

Java 自动装箱性能的相关文章

java 自动装箱拆箱实现方法

java 自动装箱拆箱实现方法 什么是自动装箱拆箱 基本数据类型的自动装箱(autoboxing).拆箱(unboxing)是自j2se 5.0开始提供的功能.  一般我们要创建一个类的对象的时候,我们会这样: class a = new class(parameter); 当我们创建一个integer对象时,却可以这样: integer i = 100; (注意:不是 int i = 100; ) 实际上,上面那句代码相当于:integer i = new integer(100); 此即基本

Java自动装箱与拆箱

  自动装箱与拆箱机制在实际使用中非常常见,不过也特别容易出错,博主在面对下面一道题的时候自信满满,可还是没有能够全对,所以写下这篇博文,给自己对自动装箱与拆箱机制做一下知识巩固,也给各位朋友做一下参考.   首先有这样一道题,给出下面代码的输出结果: public class AutoBoxing { public static void main(String[] args) { Integer a = 1; Integer b = 2; Integer c = 3; Integer d =

java自动装箱拆箱深入剖析_java

这个是jdk1.5以后才引入的新的内容,作为秉承发表是最好的记忆,毅然决定还是用一篇博客来代替我的记忆: java语言规范中说道:在许多情况下包装与解包装是由编译器自行完成的(在这种情况下包装成为装箱,解包装称为拆箱): 其实按照我自己的理解自动装箱就可以简单的理解为将基本数据类型封装为对象类型,来符合java的面向对象:例如用int来举例: 复制代码 代码如下: //声明一个Integer对象 Integer num = 10; //以上的声明就是用到了自动的装箱:解析为 Integer nu

java-JAVA 自动装箱Integer i1=100 的问题

问题描述 JAVA 自动装箱Integer i1=100 的问题 Integer i1=100; Integer i2=100; 这个时候的i1和i2是不是同一个对象?指向同一块内存? 为什么我修改i1的值 i1=101: 这个时候的i2的值会发生改变吗 解决方案 当然不会,因为是值类型. 你永远不要指望能用两个变量共享一个值类型 除非你将它定义成引用类型的一个成员变量,然后去使用这个引用类型. 解决方案二: 多说几句, Integer i1=100; Integer i2=100; 因为是值类

Java 性能要点:自动装箱/ 拆箱 (Autoboxing / Unboxing)

[编者按]本文作者为 Ali Kemal TASCI,最早于2016年4月9日发布于DZONE社区.文章主要介绍通过改进 Java 1.5 就已存在的骨灰级特性大幅度提高应用性能. 本文系 OneAPM 工程师编译呈现,以下为正文. 如果我告诉你:"只要修改一个字符,下面这段代码的运行速度就能提高5倍.",你觉得可能么? long t = System.currentTimeMillis(); Long sum = 0L;for (long i = 0; i < Integer.

Java JDK之自动装箱和拆箱

基本数据(Primitive)类型的自动装箱(autoboxing).拆箱(unboxing)是自J2SE 5.0开始提供的功能.虽然为您打包基本数据类型提供了方便,但提供方便的同时表示隐藏了细节,建议在能够区分基本数据类型与对象的差别时再使用. autoboxing和unboxing 在Java中,所有要处理的东西几乎都是对象 (Object),例如之前所使用的Scanner是对象,字符串(String)也是对象,之后还会看到更多的对象.然而基本(Primitive)数据类型不是对象,也就是您

[jjzhu学java]之自动装箱的陷阱

自动装箱.拆箱的陷阱 装箱与拆箱 java语言中为每种基本数据类型(int,float,double-)都提供了与之对应的包装器类型(Integer,Float,Double).从java se5之后就开始提供了自动装箱的特性.想要得到一个数值为2016的Integer时,只需要如下的赋值语句: //Integer a = Integer.valueOf(2016); Integer a = 2016; 该语句就会自定根据=右边的数值创建相应的Integer,这个过程就是自动装箱. 拆箱与装箱是

Java学习之自动装箱和自动拆箱源码分析

自动装箱(boxing)和自动拆箱(unboxing) 首先了解下Java的四类八种基本数据类型 基本类型 占用空间(Byte) 表示范围 包装器类型 boolean 1/8 true|false Boolean char 2 -128~127 Character byte 1 -128~127 Byte short 2 -2ˆ15~2ˆ15-1 Short int 4 -2ˆ31~2ˆ31-1 Integer long 8 -2ˆ63~2ˆ63-1 Long float 4 -3.403E38

java基础:合法转换与自动装箱、拆箱

问题描述 java基础:合法转换与自动装箱.拆箱 Short y = 100; int z = y; 这两句代码编译正确,但为什么呢?是y先拆箱成short,然后合法转换成int吗? short y1 = 100; Integer z1 = y1; 而这两句代码编译报错,如果按照上面的逻辑,可以y1先合法转换int,然后自动装箱成Integer呀? 解决方案 涉及到隐式转换. 对非引用类型赋值的时候,对基本数据类型会进行自动拆箱,然后可以隐式转换为大类型: 如第一种. 而引用类型则是先装箱再赋值