Java 自动装箱性能
Java 的基本數(shù)據(jù)類型(int、double、 char)都不是對象。但由于很多Java代碼需要處理的是對象(Object),Java給所有基本類型提供了包裝類(Integer、Double、Character)。有了自動裝箱,你可以寫如下的代碼
| 1 2 | Character boxed = 'a'; charunboxed = boxed; |
編譯器自動將它轉(zhuǎn)換為
| 1 2 | Character boxed = Character.valueOf('a'); charunboxed = boxed.charValue(); |
然而,Java虛擬機不是每次都能理解這類過程,因此要想得到好的系統(tǒng)性能,避免不必要的裝箱很關(guān)鍵。這也是 OptionalInt 和 IntStream 等特殊類型存在的原因。在這篇文章中,我將概述JVM很難消除自動裝箱的一個原因。
實例
例如,我們想要計算任意一類數(shù)據(jù)的編輯距離(Levenshtein距離),只要這些數(shù)據(jù)可以被看作一個序列:
| 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 | publicclass Levenshtein{ privatefinal Function> asList; publicLevenshtein(Function> asList) { this.asList = asList; } publicint distance(T a, T b) { // Wagner-Fischer algorithm, with two active rows List aList = asList.apply(a); List bList = asList.apply(b); intbSize = bList.size(); int[] row0 = newint[bSize + 1]; int[] row1 = newint[bSize + 1]; for(inti = 0; i row0[i] = i; } for(inti = 0; i < bSize; ++i) { U ua = aList.get(i); row1[0] = row0[0] + 1; for(intj = 0; j < bSize; ++j) { U ub = bList.get(j); intsubCost = row0[j] + (ua.equals(ub) ? 0: 1); intdelCost = row0[j + 1] + 1; intinsCost = row1[j] + 1; row1[j + 1] = Math.min(subCost, Math.min(delCost, insCost)); } int[] temp = row0; row0 = row1; row1 = temp; } returnrow0[bSize]; } } |
只要兩個對象可以被看作List,這個類就可以計算它們的編輯距離。如果想計算String類型的距離,那么就需要把String轉(zhuǎn)變?yōu)長ist類型:
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | publicclass StringAsList extendsAbstractList{ privatefinal String str; publicStringAsList(String str) { this.str = str; } @Override publicCharacter get(intindex) { returnstr.charAt(index); // Autoboxing! } @Override publicint size() { returnstr.length(); } } ... Levenshteinlev = newLevenshtein<>(StringAsList::new); lev.distance("autoboxing is fast","autoboxing is slow");// 4 |
由于Java泛型的實現(xiàn)方式,不能有List類型,所以要提供List和裝箱操作。(注:Java10中,這個限制也許會被取消。)
基準測試
為了測試 distance() 方法的性能,需要做基準測試。Java中微基準測試很難保證準確,但幸好OpenJDK提供了JMH(Java Microbenchmark Harness),它可以幫我們解決大部分難題。如果感興趣的話,推薦大家閱讀文檔和實例;它會很吸引你。以下是基準測試:
| 1 2 3 4 5 6 7 8 9 10 11 | @State(Scope.Benchmark) publicclass MyBenchmark { privateLevenshtein lev = newLevenshtein<>(StringAsList::new); @Benchmark @BenchmarkMode(Mode.AverageTime) @OutputTimeUnit(TimeUnit.NANOSECONDS) publicint timeLevenshtein() { returnlev.distance("autoboxing is fast","autoboxing is slow"); } } |
(返回方法的結(jié)果,這樣JMH就可以做一些操作讓系統(tǒng)認為返回值會被使用到,防止冗余代碼消除影響了結(jié)果。)
以下是結(jié)果:
| 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 3days ago) # VM invoker: /usr/lib/jvm/java-8-openjdk/jre/bin/java # VM options: # Warmup: 8iterations, 1s each # Measurement: 8iterations, 1s each # Timeout: 10min per iteration # Threads: 1thread, 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: 1of 1 # Warmup Iteration 1:1517.495ns/op # Warmup Iteration 2:1503.096ns/op # Warmup Iteration 3:1402.069ns/op # Warmup Iteration 4:1480.584ns/op # Warmup Iteration 5:1385.345ns/op # Warmup Iteration 6:1474.657ns/op # Warmup Iteration 7:1436.749ns/op # Warmup Iteration 8:1463.526ns/op Iteration1:1446.033ns/op Iteration2:1420.199ns/op Iteration3:1383.017ns/op Iteration4:1443.775ns/op Iteration5:1393.142ns/op Iteration6:1393.313ns/op Iteration7:1459.974ns/op Iteration8:1456.233ns/op Result"timeLevenshtein": 1424.461±(99.9%)59.574ns/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 81424.461 ± 59.574ns/op |
分析
為了查看代碼熱路徑(hot path)上的結(jié)果,JMH集成了Linux工具perf,可以查看最熱代碼塊的JIT編譯結(jié)果。(要想查看匯編代碼,需要安裝hsdis插件。我在AUR上提供了下載,Arch用戶可以直接獲取。)在JMH命令行添加 -prof perfasm 命令,就可以看到結(jié)果:
| 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 jg0x00007fde989a6148;*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 jae0x00007fde989a6103;*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) ... |
輸出內(nèi)容很多,但上面的一點內(nèi)容就說明裝箱沒有被優(yōu)化。為什么要和0x7f/0×80的內(nèi)容做比較呢?原因在于Character.valueOf()的取值來源:
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | privatestatic class CharacterCache { privateCharacterCache(){} staticfinal Character cache[] = newCharacter[127+ 1]; static{ for(inti = 0; i < cache.length; i++) cache[i] = newCharacter((char)i); } } publicstatic Character valueOf(charc) { if(c returnCharacterCache.cache[(int)c]; } returnnew Character(c); } |
可以看出,Java語法標準規(guī)定前127個char的Character對象放在緩沖池中,Character.valueOf()的結(jié)果在其中時,直接返回緩沖池的對象。這樣做的目的是減少內(nèi)存分配和垃圾回收,但在我看來這是過早的優(yōu)化。而且它妨礙了其他優(yōu)化。JVM無法確定 Character.valueOf(c).charValue() == c,因為它不知道緩沖池的內(nèi)容。所以JVM從緩沖池中取了一個Character對象并讀取它的值,結(jié)果得到的就是和 c 一樣的內(nèi)容。
解決方法
解決方法很簡單:
| 1 2 3 4 5 6 7 8 9 | @ @ -11,7+11,7@ @ publicclass StringAsList extendsAbstractList { @Override publicCharacter get(intindex) { -returnstr.charAt(index); // Autoboxing! +returnnew Character(str.charAt(index)); } @Override |
用顯式的裝箱代替自動裝箱,就避免了調(diào)用Character.valueOf(),這樣JVM就很容易理解代碼:
| 1 2 3 4 5 6 7 8 9 | privatefinal char value; publicCharacter(charvalue) { this.value = value; } publicchar charValue() { returnvalue; } |
雖然代碼中加了一個內(nèi)存分配,但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 81221.151 ± 58.878ns/op |
速度提升了14%。用 -prof perfasm 命令可以顯示,改進以后是直接從String中拿到char值并在寄存器中比較的:
| 1 2 3 4 5 6 7 8 9 | movzwl0x10(%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 je0x00007faa8d404792;*if_icmpne ; - java.lang.Character::equals@18(line 4621) ; - com.tavianator.boxperf.Levenshtein::distance@137(line 33) |
總結(jié)
裝箱是HotSpot的一個弱項,希望它能做到越來越好。它應該多利用裝箱類型的語義,消除裝箱操作,這樣以上的解決辦法就沒有必要了。
以上的基準測試代碼都可以在GitHub上訪問。
原文鏈接:? tavianator ?翻譯:? ImportNew.com? -? rainsbaby譯文鏈接:? http://www.importnew.com/16737.html
總結(jié)
以上是生活随笔為你收集整理的Java 自动装箱性能的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 实例教程:1小时学会Python
- 下一篇: 谁在关心toString的性能?