java自動裝箱性能

Java 的基本數據類型(int、double、 char)都不是對象。但因爲不少Java代碼須要處理的是對象(Object),Java給全部基本類型提供了包裝類(Integer、Double、Character)。有了自動裝箱,你能夠寫以下的代碼java

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

編譯器自動將它轉換爲web

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

然而,Java虛擬機不是每次都能理解這類過程,所以要想獲得好的系統性能,避免沒必要要的裝箱很關鍵。這也是 OptionalInt 和 IntStream 等特殊類型存在的緣由。在這篇文章中,我將概述JVM很難消除自動裝箱的一個緣由。app

實例

例如,咱們想要計算任意一類數據的編輯距離(Levenshtein距離),只要這些數據能夠被看做一個序列:jvm

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類型:ide

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 命令,就能夠看到結果:this

$ 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的一個弱項,但願它能作到愈來愈好。它應該多利用裝箱類型的語義,消除裝箱操做,這樣以上的解決辦法就沒有必要了。

相關文章
相關標籤/搜索