簡體   English   中英

當x = 0時,Java的Math.pow(x,2)表現不佳

[英]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編譯器),缺乏許多優化。 難怪它沒有像人們預期的那樣好。

  • 即使沒有副作用,客戶端VM也不夠智能,無法消除Math.pow調用。
  • 而且,對於Y=2X=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.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM