大方法的執行性能與調優過程小記

你寫過超過2500行的方法麼?一般來講,這麼大的方法並很少見,通常都是使用機器輔助生成的爲主,這種狀況在模板編譯或其它語言的自動轉換中比較常見。例如,對一個複雜的JSP頁面,機器有可能會爲它生成一個複雜的servlet方法去實現。java

然而在Hotspot上運行這種大方法,極可能會有性能問題。例如,把文章所附DEMO的play()方法的內容分別重複拷貝一、二、四、八、1六、32次並依次運行,在個人機器(Hotspot_1.6u22/Windows)上獲得的play()的執行消耗時間分別是28.4三、54.7二、106.2八、214.4一、419.30、1476.40毫秒/萬次。在重複拷貝1~16次時,隨着代碼量增長,方法執行所消耗的時間也對應成倍增長。當重複拷貝32次時,方法卻多消耗了80%的時間。若是把這個play()方法拆分紅play1()和play2(),讓它們的方法體都是16次的重複拷貝,play1()最後再調用play2(),那麼,play1()+play2()的執行消耗時間是857.75毫秒/萬次,剛好是以前重複拷貝16次所消耗的時間的兩倍。爲何一樣功能的一段代碼放在一個方法中執行會變慢,拆分紅兩個方法就變快?app

你們知道,JVM一開始是以解釋方式執行字節碼的。當這段代碼被執行的次數足夠多之後,它會被動態優化並編譯成機器碼執行,執行速度會大大加快,這就是所謂的JIT編譯。DEMO的play()方法在被統計消耗時間以前,已經預熱執行了2000次,知足ClientVM的方法JIT編譯閾值CompileThreshold=1500次的要求,那麼,它是否是真的被JIT編譯了呢?咱們能夠增長VM參數」-XX:+PrintCompilation」調查一下。在+PrintCompilation打開之後,列出了JVM在運行時進行過JIT編譯的方法。下面是通過32次重複拷貝的play()方法的JIT編譯記錄(只列出須要關心的部分):less

34       HugeMethodDemo::buildTheWorld (184 bytes)
39       HugeMethodDemo::run (59 bytes)

而分紅兩部分的play1()+plaay2()的JIT編譯記錄則爲:ide

<span style="color: #0000ff"><strong>18       HugeMethodDemo::play1 (4999 bytes)
19       HugeMethodDemo::play2 (4993 bytes)</strong></span>
36       HugeMethodDemo::buildTheWorld (184 bytes)
41       HugeMethodDemo::run (59 bytes)

顯然,通過重複拷貝32次的play()方法沒有通過JIT編譯,始終採用解釋方式執行,而分拆開的play1()+play2()通過JIT編譯,因此難怪play()要慢80%。oop

爲何play()方法不受JVM青睞呢,是太長了麼?這隻能到Hotspot源碼中去翻答案了。在compilationPolicy.cpp中有寫道:post

// Returns true if m is allowed to be compiled
bool CompilationPolicy::canBeCompiled(methodHandle m) {
if (m-&gt;is_abstract()) return false;
<span style="color: #0000ff">if (DontCompileHugeMethods &amp;&amp; m-&gt;code_size() &gt; HugeMethodLimit) return false;</span>
// Math intrinsics should never be compiled as this can lead to
// monotonicity problems because the interpreter will prefer the
// compiled code to the intrinsic version.  This can't happen in
// production because the invocation counter can't be incremented
// but we shouldn't expose the system to this problem in testing
// modes.
if (!AbstractInterpreter::can_be_compiled(m)) {
return false;
}
return !m-&gt;is_not_compilable();
}

當DontCompileHugeMethods=true且代碼長度大於HugeMethodLimit時,方法不會被編譯。DontCompileHugeMethods與HugeMethodLimit的值在globals.hpp中定義:性能

上面兩個參數說明了Hotspot對字節碼超過8000字節的大方法有JIT編譯限制,這就是play()杯具的緣由。因爲使用的是productmode的JRE,咱們只能嘗試關閉DontCompileHugeMethods,即增長VM參數」-XX:-DontCompileHugeMethods」來強迫JVM編譯play()。再次對play()進行測試,耗時855毫秒/萬次,性能終於上來了,輸出的JIT編譯記錄也增長了一行:測試

16       HugeMethodDemo::play (9985 bytes)

使用」-XX:-DontCompileHugeMethods」解除大方法的編譯限制,一個比較明顯的缺點是JVM會嘗試編譯所遇到的全部大方法,者會使JIT編譯任務負擔更重,並且須要佔用更多的CodeCache區域去保存編譯後的代碼。可是優勢是編譯後可讓大方法的執行速度變快,且可能提升GC速度。運行時CodeCache的使用量能夠經過JMX或者JConsole得到,CodeCache的大小在globals.hpp中定義:優化

define_pd_global(intx, ReservedCodeCacheSize, 48*M);
product_pd(uintx, InitialCodeCacheSize, "Initial code cache size (in bytes)")
product_pd(uintx, ReservedCodeCacheSize, "Reserved code cache size (in bytes) - maximum code cache size")
product(uintx, CodeCacheMinimumFreeSpace, 500*K, "When less than X space left, we stop compiling.")

一旦CodeCache滿了,HotSpot會中止全部後續的編譯任務,雖然已編譯的代碼不受影響,可是後面的全部方法都會強制停留在純解釋模式。所以,如非必要,應該儘可能避免生成大方法;若是解除了大方法的編譯限制,則要留意配置CodeCache區的大小,準備更多空間存放編譯後的代碼。ui

最後附上DEMO代碼:

[java]
import java.io.StringWriter;
import java.io.Writer;
import java.util.HashMap;
import java.util.Map;
public class HugeMethodDemo {
public static void main(String[] args) throws Exception {
HugeMethodDemo demo = new HugeMethodDemo();
int warmup = 2000;
demo.run(warmup);
int loop = 200000;
double total = demo.run(loop);
double avg = total / loop / 1e6 * 1e4;
System.out.println(String.format(
"Loop=%d次, " + "avg=%.2f毫秒/萬次", loop, avg));
}
private long run(int loop) throws Exception {
long total = 0L;
for (int i = 0; i < loop; i++) {
Map theWorld = buildTheWorld();
StringWriter console = new StringWriter();
long start = System.nanoTime();
play(theWorld, console);
long end = System.nanoTime();
total += (end – start);
}
return total;
}
private Map buildTheWorld() {
Map context = new HashMap();
context.put("name", "D&D");
context.put("version", "1.0");
Map game = new HashMap();
context.put("game", game);
Map player = new HashMap();
game.put("player", player);
player.put("level", "26");
player.put("name", "jifeng");
player.put("job", "paladin");
player.put("address", "heaven");
player.put("weapon", "sword");
player.put("hp", 150);
String[] bag = new String[] { "world_map", "dagger",
"magic_1", "potion_1", "postion_2", "key" };
player.put("bag", bag);
return context;
}
private void play(Map theWorld, Writer console) throws Exception {
// 重複拷貝的開始位置
if (true) {
String name = String.valueOf(theWorld.get("name"));
String version = String.valueOf(theWorld.get("version"));
console.append("Game ").append(name).append(" (v").append(version).append(")n");
Map game = (Map) theWorld.get("game");
if (game != null) {
Map player = (Map) game.get("player");
if (player != null) {
String level = String.valueOf(player.get("level"));
String job = String.valueOf(player.get("job"));
String address = String.valueOf(player.get("address"));
String weapon = String.valueOf(player.get("weapon"));
String hp = String.valueOf(player.get("hp"));
console.append("  You are a ").append(level).append(" level ").append(job)
.append(" from ").append(address).append(". n");
console.append("  Currently you have a ").append(weapon).append(" in hand, ")
.append("your hp: ").append(hp).append(". n");
console.append("  Here are items in your bag: n");
for (String item : (String[]) player.get("bag")) {
console.append("     * ").append(item).append("n");
}
} else {
console.append("tPlayer not login.n");
}
} else {
console.append("tGame not start yet.n");
}
}
// 重複拷貝的結束位置
}
}
[/java]
相關文章
相關標籤/搜索