[英]Poor performance of Java's Math.pow(x, 2) when x = 0
背景
注意到我正在處理的java程序的執行速度比預期慢,我決定修補我認為可能導致問題的代碼區域 - 從一個內部調用Math.pow(x,2) for循環。 與本網站上的另一個問題相反,我創建的一個簡單的基准測試(最后的代碼)發現用x * x替換Math.pow(x,2)實際上將循環加速了近70倍:
x*x: 5.139383ms
Math.pow(x, 2): 334.541166ms
請注意,我知道基准測試並不完美,而且值肯定應該用一點點鹽 - 基准的目的是得到一個大概的數字。
問題
雖然基准測試給出了有趣的結果,但它沒有准確地模擬我的數據,因為我的數據主要由0組成。 因此,更准確的測試是運行基准測試,而不將for循環標記為可選。 根據Math.pow()的javadoc
如果第一個參數為正零且第二個參數大於零,或者第一個參數為正無窮大且第二個參數小於零,則結果為正零。
所以預計這個基准測試運行得更快吧!? 但實際上,這又慢了很多:
x*x: 4.3490535ms
Math.pow(x, 2): 3082.1720006ms
當然,人們可能期望math.pow()代碼比簡單的x * x代碼運行慢一點,因為它需要適用於一般情況,但速度要慢700倍? 到底是怎么回事!? 為什么0情況比Math.random()情況慢得多?
更新:根據@Stephen C的建議更新代碼和時間。 然而,這沒有什么區別。
代碼用於基准測試
請注意,重新排序這兩個測試可以忽略不計。
public class Test {
public Test(){
int iterations = 100;
double[] exampleData = new double[5000000];
double[] test1Results = new double[iterations];
double[] test2Results = new double[iterations];
//Optional
for (int i = 0; i < exampleData.length; i++) {
exampleData[i] = Math.random();
}
for (int i = 0; i < iterations; i++) {
test1Results[i] = test1(exampleData);
test2Results[i] = test2(exampleData);
}
System.out.println("x*x: " + calculateAverage(test1Results) / 1000000 + "ms");
System.out.println("Math.pow(x, 2): " + calculateAverage(test2Results) / 1000000 + "ms");
}
private long test1(double[] exampleData){
double total = 0;
long startTime;
long endTime;
startTime = System.nanoTime();
for (int j = 0; j < exampleData.length; j++) {
total += exampleData[j] * exampleData[j];
}
endTime = System.nanoTime();
System.out.println(total);
return endTime - startTime;
}
private long test2(double[] exampleData){
double total = 0;
long startTime;
long endTime;
startTime = System.nanoTime();
for (int j = 0; j < exampleData.length; j++) {
total += Math.pow(exampleData[j], 2);
}
endTime = System.nanoTime();
System.out.println(total);
return endTime - startTime;
}
private double calculateAverage(double[] array){
double total = 0;
for (int i = 0; i < array.length; i++) {
total += array[i];
}
return total/array.length;
}
public static void main(String[] args){
new Test();
}
}
雖然這是一個糟糕的基准,但它幸運地揭示了一個有趣的效果。
這些數字表明您顯然在“客戶端”VM下運行基准測試。 它沒有非常強大的JIT編譯器(稱為C1編譯器),缺乏許多優化。 難怪它沒有像人們預期的那樣好。
Math.pow
調用。 Y=2
和X=0
,它沒有專門的快速路徑。 至少,它直到Java 9才有。最近在JDK-8063086中修復了這個問題 ,然后在JDK-8132207中進一步優化。 但有趣的是,使用C1編譯器, X=0
Math.pow
確實更慢!
但為什么? 由於實施細節。
x86體系結構不提供計算X ^ Y的硬件指令。 但還有其他有用的說明:
FYL2X
計算Y *log₂X F2XM1
計算2 ^ X - 1 由此,X ^ Y = 2 ^(Y * log 2 X)。 由於log 2X僅定義為X> 0,因此FYL2X
最終會出現X=0
的異常並返回-Inf
。 因此,在慢速異常路徑中而不是在專用快速路徑中處理X=0
。
那么該怎么辦?
首先,停止使用客戶端VM,特別是如果您關心性能。 切換到64位版本的最新JDK 8,您將獲得最佳的C2優化JIT編譯器。 當然,它很好地處理Math.pow(x, 2)
等。然后使用像JMH這樣的適當工具編寫正確的基准 。
可能與JDK 7中的這種回歸有關: http : //bugs.java.com/bugdatabase/view_bug.do?video_id = 8029302
從錯誤報告:
有一個Math.pow性能回歸,其中2個輸入的功率比其他值慢。
我已經替換了對Math.pow()的所有調用,因為:
public static double pow(final double a, final double b) {
if (b == 2.0) {
return (a * a);
} else {
return Math.pow(a, b);
}
}
根據錯誤報告,它已在JDK 8中修復,符合上面的@BartKiers評論。
雖然@whiskeyspider發現的錯誤報告是相關的,但我不認為這是完整的解釋。 根據錯誤報告,回歸大約減少4倍。 但在這里,我們看到大約1000倍的放緩。 差異太大,不容忽視。
我認為我們在這里看到的部分問題是基准測試本身。 看這個:
for (int j = 0; j < exampleData.length; j++) {
double output = exampleData[j] * exampleData[j];
}
正文中的語句分配給未使用的局部變量。 它可以通過JIT編譯器進行優化。 (事實上,整個循環可以被優化掉......雖然在經驗上似乎沒有發生這種情況。)
相比之下:
for (int j = 0; j < exampleData.length; j++) {
double output = Math.pow(exampleData[j], 2);
}
除非JIT編譯器知道pow
是無副作用的,否則無法進行優化。 由於pow
的實現是在本機代碼中,因此必須以該方法被“內在”......引導的方式傳授該知識。 從錯誤報告分析來看,不同Java版本/發行版之間“內部化”的變化是回歸的根本原因。 我懷疑OP的基准測試中的缺陷正在放大效果。
修復是為了確保使用output
值,以便JIT編譯器無法優化它; 例如
double blackhole = 0; // declared at start ...
...
for (int j = 0; j < exampleData.length; j++) {
blackhole += exampleData[j] * exampleData[j];
}
...
for (int j = 0; j < exampleData.length; j++) {
blackhole += Math.pow(exampleData[j], 2);
}
參考: https : //stackoverflow.com/a/513259/139985 ...特別是規則#6。
使用Math類中的任何方法需要更長的時間,然后只使用一個簡單的運算符(如果可能)。 這是因為程序必須傳遞Math的輸入。 在Math類的method ()中,Math類將執行操作,然后Math類將返回從Math計算的值。 方法 ()。 所有這些都需要比使用*,/,+或 - 等基本運算符更多的處理能力。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.