3 回答

TA貢獻1942條經(jīng)驗 獲得超3個贊
您正在測量OSR(堆棧上替換)存根。
OSR 存根是一種特殊版本的編譯方法,專門用于在方法運行時將執(zhí)行從解釋模式轉(zhuǎn)移到編譯代碼。
OSR 存根不像常規(guī)方法那樣優(yōu)化,因為它們需要與解釋幀兼容的幀布局。我已經(jīng)在以下答案中展示了這一點:1 , 2 , 3。
類似的事情也發(fā)生在這里。當“低效代碼”運行一個長循環(huán)時,該方法是專門為循環(huán)內(nèi)的堆棧替換而編譯的。狀態(tài)從解釋幀轉(zhuǎn)移到 OSR 編譯方法,該狀態(tài)包括progressCheck局部變量。此時 JIT 無法用常量替換變量,因此無法應用某些優(yōu)化,如強度降低。
特別是這意味著 JIT 不會用乘法代替整數(shù)除法。(請參閱為什么 GCC 在實現(xiàn)整數(shù)除法時使用乘以一個奇怪的數(shù)字?對于提前編譯器的 asm 技巧,當值是內(nèi)聯(lián)/常量傳播后的編譯時常量時,如果啟用了這些優(yōu)化.表達式中的整數(shù)文字也通過 優(yōu)化,類似于此處由 JITer 優(yōu)化的地方,即使在 OSR 存根中也是如此。)%gcc -O0
但是,如果您多次運行相同的方法,則第二次和后續(xù)運行將執(zhí)行常規(guī)(非 OSR)代碼,這是完全優(yōu)化的。這是證明理論的基準(使用 JMH 進行基準測試):
@State(Scope.Benchmark)
public class Div {
@Benchmark
public void divConst(Blackhole blackhole) {
long startNum = 0;
long stopNum = 100000000L;
for (long i = startNum; i <= stopNum; i++) {
if (i % 50000 == 0) {
blackhole.consume(i);
}
}
}
@Benchmark
public void divVar(Blackhole blackhole) {
long startNum = 0;
long stopNum = 100000000L;
long progressCheck = 50000;
for (long i = startNum; i <= stopNum; i++) {
if (i % progressCheck == 0) {
blackhole.consume(i);
}
}
}
}
結(jié)果:
# Benchmark: bench.Div.divConst
# Run progress: 0,00% complete, ETA 00:00:16
# Fork: 1 of 1
# Warmup Iteration 1: 126,967 ms/op
# Warmup Iteration 2: 105,660 ms/op
# Warmup Iteration 3: 106,205 ms/op
Iteration 1: 105,620 ms/op
Iteration 2: 105,789 ms/op
Iteration 3: 105,915 ms/op
Iteration 4: 105,629 ms/op
Iteration 5: 105,632 ms/op
# Benchmark: bench.Div.divVar
# Run progress: 50,00% complete, ETA 00:00:09
# Fork: 1 of 1
# Warmup Iteration 1: 844,708 ms/op <-- much slower!
# Warmup Iteration 2: 105,893 ms/op <-- as fast as divConst
# Warmup Iteration 3: 105,601 ms/op
Iteration 1: 105,570 ms/op
Iteration 2: 105,475 ms/op
Iteration 3: 105,702 ms/op
Iteration 4: 105,535 ms/op
Iteration 5: 105,766 ms/op
由于 OSR 存根編譯效率低下,第一次迭代divVar確實慢得多。但是,只要方法從頭開始重新運行,就會執(zhí)行新的不受約束的版本,該版本會利用所有可用的編譯器優(yōu)化。

TA貢獻1871條經(jīng)驗 獲得超8個贊
在跟進@phuclv comment時,我檢查了JIT 1生成的代碼,結(jié)果如下:
對于variable % 5000(除以常數(shù)):
mov rax,29f16b11c6d1e109h
imul rbx
mov r10,rbx
sar r10,3fh
sar rdx,0dh
sub rdx,r10
imul r10,rdx,0c350h ; <-- imul
mov r11,rbx
sub r11,r10
test r11,r11
jne 1d707ad14a0h
對于variable % variable:
mov rax,r14
mov rdx,8000000000000000h
cmp rax,rdx
jne 22ccce218edh
xor edx,edx
cmp rbx,0ffffffffffffffffh
je 22ccce218f2h
cqo
idiv rax,rbx ; <-- idiv
test rdx,rdx
jne 22ccce218c0h
因為除法總是比乘法花費更長的時間,所以最后一個代碼片段的性能較低。
爪哇版:
java version "11" 2018-09-25
Java(TM) SE Runtime Environment 18.9 (build 11+28)
Java HotSpot(TM) 64-Bit Server VM 18.9 (build 11+28, mixed mode)

TA貢獻1828條經(jīng)驗 獲得超3個贊
正如其他人所指出的,一般的模運算需要進行除法。在某些情況下,除法可以(由編譯器)用乘法代替。但與加法/減法相比,兩者都可能很慢。因此,可以通過以下方式獲得最佳性能:
long progressCheck = 50000;
long counter = progressCheck;
for (long i = startNum; i <= stopNum; i++){
if (--counter == 0) {
System.out.println(i);
counter = progressCheck;
}
}
(作為一個小的優(yōu)化嘗試,我們在這里使用一個預遞減遞減計數(shù)器,因為在許多架構上0,與算術運算之后的立即比較成本正好為 0 指令/CPU 周期,因為 ALU 的標志已經(jīng)由前面的操作適當?shù)卦O置。一個體面的優(yōu)化但是,即使您編寫了 .,編譯器也會自動進行優(yōu)化if (counter++ == 50000) { ... counter = 0; }。)
i請注意,您通常并不真正想要/需要模數(shù),因為您知道循環(huán)計數(shù)器(如果加一計數(shù)器達到某個值。
另一個“技巧”是使用二次冪值/限制,例如progressCheck = 1024;. 模數(shù) 2 的冪可以通過按位快速計算and,即if ( (i & (1024-1)) == 0 ) {...}. 這也應該很快,并且在某些架構上可能會優(yōu)于上面的顯式counter。
添加回答
舉報