2011-07-06 59 views
14

我一直在看一些Java原始集合(trove,fastutil,hppc),我注意到一個模式,類變量有時被聲明爲final局部變量。例如:在Java中訪問最終本地變量比類變量快嗎?

public void forEach(IntIntProcedure p) { 
    final boolean[] used = this.used; 
    final int[] key = this.key; 
    final int[] value = this.value; 
    for (int i = 0; i < used.length; i++) { 
     if (used[i]) { 
      p.apply(key[i],value[i]); 
     } 
    } 
} 

我做了一些基準測試,看來這是稍快執行此操作時,偏偏是這樣的話?我試圖理解如果函數的前三行被註釋掉了,Java會做什麼不同。

注意:這似乎與this question類似,但這是針對C++的,並沒有解決爲什麼它們被聲明爲final

+1

您可以嘗試查看生成的java程序集以查看差異。 –

+0

剛剛意識到,原因可能是在HotSpot編譯器中,而不是字節碼本身... –

+0

請發佈您的基準測試代碼,至少有一些機會,你錯誤地基準測試方法,實際上只測試解釋器而不是編譯器:) – Voo

回答

8

final關鍵字在這裏是一個紅色的鯡魚。 性能差異是因爲他們說了兩件不同的事情。

public void forEach(IntIntProcedure p) { 
    final boolean[] used = this.used; 
    for (int i = 0; i < used.length; i++) { 
    ... 
    } 
} 

是在說,「取一個布爾數組,併爲陣列中的每個元素做一些事情。」

沒有final boolean[] used,功能說「,而指數小於當前對象的used領域的當前值的長度,取當前對象的used領域的當前值,做一些與元素在索引i「。

JIT可能有一個更容易的時間驗證循環限制不變量以消除過度限制檢查等,因爲它可以更容易地確定什麼會導致used的值改變。即使忽略多個線程,如果p.apply可能會更改used的值,那麼JIT無法消除邊界檢查或進行其他有用的優化。

+0

我對'final'是什麼意思是一個紅鯡魚感到困惑。你的意思是訪問變量不一定更快,但JIT編譯器可以優化循環以消除範圍檢查和查找? – job

+0

「即使忽略多個線程」 - 只是爲了清楚:JIT ** only **會考慮線程本地行爲。這意味着即使使用的是公開的(或者有一個setter方法)並且可能被另一個線程改變,JIT也有權忽略這個。所以JIT只需要弄清楚apply()是否會改變引用(在實踐中:如果它可以內聯該調用(以及所有的subcalls),它就會注意到它,否則你肯定會失敗) – Voo

+0

此外,很有可能出現「更快」的行爲,因爲有人再次寫了一個無效的java基準測試(太容易了,太難以正確了) - 解釋器中應該存在性能差異,但如果應用非常簡單,編譯代碼與現代Hotspot – Voo

2

它告訴運行時(jit),在那個方法調用的上下文中,這3個值永遠不會改變,所以運行時不需要不斷地從成員變量加載值。這可能會略微提高速度。

當然,隨着jit變得更聰明並且可以自行計算出這些東西,這些約定變得不那麼有用。

請注意,我沒有說清楚加速比使用局部變量更多的是比最後一部分。

+0

嘿,我也在打字! :-)除了我認爲即使編譯器可以從知道該方法對這些引用中的並行更改不感興趣中受益。 – Szocske

25

訪問局部變量或參數是一個單步操作:取一個位於棧上偏移量爲N的變量。如果函數具有2個參數(簡化):

  • N = 0 - this
  • N = 1 - 第一個參數
  • N = 2 - 第二個參數
  • N = 3 - 第一局部變量
  • N = 4 - 第二局部變量
  • ...

所以,當你訪問局部變量時,你有一個固定偏移量的存儲器訪問(N在編譯時已知)。這是第一次訪問方法參數(int)字節碼:

iload 1 //N = 1 

但是當你訪問現場,你實際上是在執行一個額外的步驟。首先,您正在閱讀「本地變量this只是爲了確定當前的對象地址。然後您正在加載一個與this有固定偏差的字段(getfield)。所以你執行兩個內存操作而不是一個(或一個額外的)。字節代碼:

aload 0 //N = 0: this reference 
getfield total I //int total 

因此技術上訪問本地變量和參數比對象字段快。在實踐中,許多其他因素可能會影響性能(包括各種級別的CPU緩存和JVM優化)。

final是一個不同的故事。這基本上是編譯器/ JIT的一個提示,這個引用不會改變,所以它可以進行更重的優化。但這很難追查到,根據經驗,儘可能使用final

+5

我想這個答案(尤其是最後一段)比標記的更好。 –

+0

我不得不懷疑,最終的一些加速可能是智能JIT可以知道在對象超出範圍之前重新使用指針,並保存在alloc()上,並且由於具有更小的內存而獲得更好的緩存命中足跡... – Ajax

+0

完全同意。最有用的答案。 – omniyo

1

在生成的VM操作碼中,局部變量是操作數堆棧上的條目,而字段引用必須通過通過對象引用檢索值的指令移動到堆棧。我想JIT可以更容易地使堆棧引用寄存器引用。

+2

不完全正確。局部變量放置在線程*堆棧*上,而不放在*操作數堆棧*上。各種'load' /'store'操作碼用於將本地變量從堆棧移到操作數堆棧並返回。請參閱[此圖片](http://www.ibm.com/developerworks/ibm/library/it-haggar_bytecode/fig01.gif)。 –

0

這種簡單的優化已經包含在JVM運行時中。如果JVM對實例變量進行天真的訪問,我們的Java應用程序將會變得很慢。

儘管這樣的手動調整對於簡單的JVM可能是值得的, Android系統。

+0

dex(android)字節碼可能更有效......未壓縮的.dx比jar壓縮的.class更小,並且java移動版的dalvik的全部原因是性能(標準jvm對於移動設備來說太笨拙) – Ajax