Java 自動裝箱效能
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 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型別:
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/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 命令,就可以看到結果:
$ 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:16 Benchmark 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的一個弱項,希望它能做到越來越好。它應該多利用裝箱型別的語義,消除裝箱操作,這樣以上的解決辦法就沒有必要了。
以上的基準測試程式碼都可以在GitHub上訪問。
相關文章
- Java 效能筆記:自動裝箱/拆箱Java筆記
- Java自動拆箱與裝箱Java
- Java中的自動裝箱與自動拆箱Java
- Java的自動裝箱和拆箱Java
- Java無意識自動裝箱嚴重消耗效能Java
- Java自動裝箱/拆箱 - Java那些事兒Java
- Java 效能要點:自動裝箱/ 拆箱 (Autoboxing / Unboxing)Java
- Java中的自動裝箱與拆箱Java
- 如何理解Java中的自動拆箱和自動裝箱?Java
- Java語法糖2:自動裝箱和自動拆箱Java
- Java基本型別自動裝箱的效能成本 -Coffee TalkJava型別
- Java學習之自動裝箱和自動拆箱原始碼分析Java原始碼
- 通過原始碼瞭解Java的自動裝箱拆箱原始碼Java
- 【java】JDK5的新特性→→自動裝箱和拆箱JavaJDK
- java中的內部類和自動拆裝箱Java
- java裝箱拆箱Java
- 夯實Java基礎系列2:Java自動拆裝箱裡隱藏的秘密Java
- 一文讀懂什麼是Java中的自動拆裝箱Java
- [譯]Kotlin中內聯類的自動裝箱和高效能探索(二)Kotlin
- [JAVA] Java物件導向之包裝類,拆箱、裝箱Java物件
- Integer 自動拆箱封箱
- 深入理解Java之裝箱與拆箱Java
- 深入剖析Java中的裝箱和拆箱Java
- 裝箱演算法的效能測試演算法
- 夯實Java基礎系列2:Java基本資料型別,以及自動拆裝箱裡隱藏的秘密Java資料型別
- 夯實Java基礎系列2:Java基本資料型別,以及自動拆裝箱裡隱藏的祕密Java資料型別
- java空指標出現的情況:拆箱裝箱Java指標
- 自動註冊gmail郵箱構想AI
- 記一次Java自動拆箱引發的空指標問題Java指標
- MySQL 自動備份併傳送到郵箱MySql
- 基礎鞏固、探尋Java裝箱和拆箱的奧妙!Java
- c#之裝箱和取消裝箱C#
- jQuery 郵箱輸入字尾自動補全jQuery
- jquery 實現郵箱輸入自動提示功能jQuery
- C#之拆箱,裝箱C#
- c#的裝箱和拆箱C#
- 郵箱輸入實現型別自動提示功能型別
- java自動化——web自動化複習JavaWeb