天天看點

為什麼 Java 中 2*(i*i) 比 2*i*i 更快?

回複”666“擷取公衆号專屬資料

為什麼 Java 中 2*(i*i) 比 2*i*i 更快?

有人在 stack overflow 上提問,為什麼 java 中的  2 * (i * i)  比  2 * i * i  要快?

他做了如下測試:

運作下面這段java代碼平均需要0.50到0.55秒:

如果把2 *(i * i)替換成2 * i * i,那麼運作時間在0.60到0.65秒之間。為什麼出現這樣的結果?

我把程式的每個版本運作了15次,兩次之間交替運作。結果如下:

2 * i * i的最快運作時間比2 * (i * i)最慢運作時間還要長。如果兩者效率相當,發生這種情況的可能性小于1/2^15 * 100% = 0.00305%。

來自 rustyx 的回答,獲得 1172 贊同

兩種方式的位元組碼順序略有不同。

2 * (i * i):

對比2 * i * i:

乍看之下沒有什麼不同,如果有的話,第二個版本看起來少了一個slot。

是以,需要更深入研究底層(jit)。

請記住,對小循環jit會主動展開。對2 * (i * i)可以看到實際展開了16x:

從上面的代碼可以看到,有1個寄存器被“spill”到了整個堆棧。

對于2 * i * i版本:

出于儲存中間結果的需要,這裡出現了更多的“spill”及堆棧[rsp + …]通路。

問題的答案很簡單:2 *(i * i)比2 * i * i更快,因為針對前者jit生成的彙編代碼更優化。

但是,顯然這兩個版本都不夠好。由于x86-64 cpu都至少支援sse2,是以循環可以從向量化中受益。

是以,這是optimizer的問題:通常循環過度展開會帶來問題,錯失其他優化機會。

實際上,現代x86-64 cpu會把指令進一步細分為微操作(µops)。循環優化可以借助寄存器重命名、µop緩存和循環緩沖區等衆多特性,而不是僅僅做一次展開。根據agner fog的優化指南:

如果平均指令長度超過4位元組,由于µop緩存而導緻的性能提升會非常可觀。可以考慮下列方法優化µop緩存:

確定關鍵循環足夠小以适應µop緩存。

将最關鍵的循環條目和功能條目以32對齊。

避免不必要的循環展開。

避免使用需要額外加載時間的指令:..

考慮到加載時間:即使命中最快的l1d也要花費4個周期,需要一個額外的寄存器和µop。隻要對存儲器通路,哪怕幾次也會損害循環的性能。

再考慮矢量化方案:要了解優化能達到多快,可以使用gcc編譯類似的c應用程式,直接對其進行矢量化(下面展示了avx2、sse2結果):

運作時間:

sse:0.24 s,大約快2倍。

avx:0.15 s,大約快3倍。

avx2:0.08 s,大約快5倍。

要輸出jit生成的程式集,請擷取jvm調試版本,并使用-xx:+ printoptoassembly運作。

c程式版本使用-fwrapv标志進行編譯,該标志使gcc可以将帶符号整數溢出視為二進制補碼。

翻譯: 唐尤華stackoverflow.com/questions/53452713/why-is-2-i-i-faster-than-2-i-i-in-java

為什麼 Java 中 2*(i*i) 比 2*i*i 更快?

想知道更多?掃描下面的二維碼關注我

為什麼 Java 中 2*(i*i) 比 2*i*i 更快?