2010-11-20 49 views

Trả lời

59

Các Javadoc cho lớp Math cung cấp một số thông tin về sự khác biệt giữa hai lớp:

Không giống như một số phương pháp số của lớp StrictMath, tất cả hiện thực của các hàm tương đương của lớp Math không được xác định để trả lại kết quả tương tự bit cho bit . Giấy phép thư giãn hoạt động tốt hơn này hoạt động tốt hơn các triển khai nghiêm ngặt không yêu cầu khả năng tái lặp .

Theo mặc định rất nhiều các phương pháp Math chỉ đơn giản gọi phương thức tương đương trong StrictMath để thực hiện. Trình tạo mã khuyến khích sử dụng thư viện gốc cụ thể cho nền tảng hoặc hướng dẫn bộ vi xử lý, trong đó có sẵn, để cung cấp triển khai hiệu suất cao hơn Math phương pháp. Các triển khai hiệu suất cao hơn như vậy vẫn phải tuân theo đặc điểm kỹ thuật cho Math.

Do đó, lớp Math đưa ra một số quy tắc về những gì hoạt động nhất định nên làm, nhưng họ không đòi hỏi rằng chính xác kết quả tương tự được trả lại trong mọi cài đặt các thư viện.

Điều này cho phép thực hiện cụ thể các thư viện để trả về tương tự, nhưng không phải kết quả tương tự chính xác nếu, ví dụ: lớp Math.cos được gọi. Điều này sẽ cho phép các triển khai nền tảng cụ thể (chẳng hạn như sử dụng điểm nổi x86 và, điểm nổi SPARC) có thể trả về các kết quả khác nhau.

(Tham khảo phần Software Implementations của Sine bài viết trong Wikipedia cho một số ví dụ về triển khai nền tảng cụ thể.)

Tuy nhiên, với StrictMath, kết quả trả về bởi hiện thực khác nhau phải trả lại kết quả tương tự . Điều này sẽ là điều mong muốn đối với các trường hợp cần phải có khả năng tái tạo kết quả trên các nền tảng khác nhau.

+1

Nhưng tại sao các triển khai nền tảng cụ thể khác nhau lại muốn tạo ra kết quả khác nhau? Không phải cosin được định nghĩa chung? – Aivar

+0

@Aivar: Vì những lý do được liệt kê trong báo giá từ lớp 'Toán' - để tận dụng các phương pháp gốc có sẵn cho nền tảng cụ thể, (trong nhiều trường hợp có thể) nhanh hơn sử dụng giải pháp dựa trên phần mềm được đảm bảo cung cấp chính xác cùng một câu trả lời trên tất cả các nền tảng. – coobird

+0

ok, do đó, nó có nghĩa là một số nền tảng đã chọn không tính toán câu trả lời chính xác nhất phù hợp với số tiền nhất định của bit, nhưng đã giao dịch chính xác cho hiệu quả? Và các nền tảng khác nhau đã thực hiện các giao dịch khác nhau? – Aivar

18

Bạn đã kiểm tra mã nguồn chưa? Nhiều phương thức trong số java.lang.Math được ủy quyền cho java.lang.StrictMath.

Ví dụ:

public static double cos(double a) { 
    return StrictMath.cos(a); // default impl. delegates to StrictMath 
} 
+14

+1 để đọc mã nguồn Java. Đây là một điểm mạnh cho Java trên .NET: một phần lớn mã nguồn cho các Java API gửi cùng với JDK trong một tệp có tên là src.zip. Và những gì không có ở đây có thể được tải xuống ngay bây giờ khi JVM mở nguồn. Việc đọc nguồn Java có thể không phải là cách được quảng cáo nhiều nhất để giải quyết các vấn đề: nó có vẻ như là một ý tưởng tồi vì bạn thường phải "tuân theo giao diện công cộng chứ không phải là thực hiện". Tuy nhiên, việc đọc nguồn có một lợi thế mạnh: nó sẽ luôn cho bạn sự thật. Và đôi khi đó là điều có giá trị nhất của tất cả. –

+4

@MikeClark Bạn cũng có thể đọc nguồn cho .NET. –

+1

@Andrew Cảm ơn lời khuyên. Tôi vừa đọc xong một hướng dẫn về cách thiết lập điều đó trong Visual Studio. Java vẫn có thể có một lợi thế nhỏ ở chỗ bạn có thể tải mã nguồn cho bản thân máy ảo, không chỉ là thư viện chuẩn của nó (khung công tác). Dù sao cũng cảm ơn! –

7

@ntoskrnl Là ai đó đang làm việc với JVM internals, tôi muốn thứ hai ý kiến ​​của bạn rằng "nội tại không nhất thiết phải hành xử theo cùng một cách như phương pháp StrictMath". Để tìm hiểu (hoặc chứng minh) nó, chúng ta chỉ có thể viết một bài kiểm tra đơn giản.

Lấy Math.pow ví dụ, kiểm tra mã Java cho java.lang.Math.pow (double a, double b), chúng ta sẽ thấy:

public static double pow(double a, double b) { 
    return StrictMath.pow(a, b); // default impl. delegates to StrictMath 
} 

Nhưng JVM là miễn phí để thực hiện nó với intrinsics hoặc cuộc gọi thời gian chạy, do đó kết quả trả về có thể khác với những gì chúng ta mong chờ từ StrictMath.pow.

Và đoạn mã sau cho thấy điều này gọi Math.pow() chống StrictMath.pow()

//Strict.java, testing StrictMath.pow against Math.pow 
import java.util.Random; 
public class Strict { 
    static double testIt(double x, double y) { 
     return Math.pow(x, y); 
    } 
    public static void main(String[] args) throws Exception{ 
     final double[] vs = new double[100]; 
     final double[] xs = new double[100]; 
     final double[] ys = new double[100]; 
     final Random random = new Random(); 

     // compute StrictMath.pow results; 
     for (int i = 0; i<100; i++) { 
      xs[i] = random.nextDouble(); 
      ys[i] = random.nextDouble(); 
      vs[i] = StrictMath.pow(xs[i], ys[i]); 
     } 
     boolean printed_compiled = false; 
     boolean ever_diff = false; 
     long len = 1000000; 
     long start; 
     long elapsed; 
     while (true) { 
      start = System.currentTimeMillis(); 
      double blackhole = 0; 
      for (int i = 0; i < len; i++) { 
       int idx = i % 100; 
       double res = testIt(xs[idx], ys[idx]); 
       if (i >= 0 && i<100) { 
        //presumably interpreted 
        if (vs[idx] != res && (!Double.isNaN(res) || !Double.isNaN(vs[idx]))) { 
         System.out.println(idx + ":\tInterpreted:" + xs[idx] + "^" + ys[idx] + "=" + res); 
         System.out.println(idx + ":\tStrict pow : " + xs[idx] + "^" + ys[idx] + "=" + vs[idx] + "\n"); 
        } 
       } 
       if (i >= 250000 && i<250100 && !printed_compiled) { 
        //presumably compiled at this time 
        if (vs[idx] != res && (!Double.isNaN(res) || !Double.isNaN(vs[idx]))) { 
         System.out.println(idx + ":\tcompiled :" + xs[idx] + "^" + ys[idx] + "=" + res); 
         System.out.println(idx + ":\tStrict pow :" + xs[idx] + "^" + ys[idx] + "=" + vs[idx] + "\n"); 
         ever_diff = true; 
        } 
       } 
      } 
      elapsed = System.currentTimeMillis() - start; 
      System.out.println(elapsed + " ms "); 
      if (!printed_compiled && ever_diff) { 
       printed_compiled = true; 
       return; 
      } 

     } 
    } 
} 

Tôi chạy thử nghiệm này với OpenJDK 8u5-B31 và nhận được kết quả dưới đây:

10: Interpreted:0.1845936372497491^0.01608930867480518=0.9731817015518033 
10: Strict pow : 0.1845936372497491^0.01608930867480518=0.9731817015518032 

41: Interpreted:0.7281259501809544^0.9414406865385655=0.7417808233050295 
41: Strict pow : 0.7281259501809544^0.9414406865385655=0.7417808233050294 

49: Interpreted:0.0727813262968815^0.09866028976654662=0.7721942440239148 
49: Strict pow : 0.0727813262968815^0.09866028976654662=0.7721942440239149 

70: Interpreted:0.6574309575966407^0.759887845481148=0.7270872740201638 
70: Strict pow : 0.6574309575966407^0.759887845481148=0.7270872740201637 

82: Interpreted:0.08662340816125613^0.4216580281197062=0.3564883826345057 
82: Strict pow : 0.08662340816125613^0.4216580281197062=0.3564883826345058 

92: Interpreted:0.20224488115245098^0.7158182878844233=0.31851834311978916 
92: Strict pow : 0.20224488115245098^0.7158182878844233=0.3185183431197892 

10: compiled :0.1845936372497491^0.01608930867480518=0.9731817015518033 
10: Strict pow :0.1845936372497491^0.01608930867480518=0.9731817015518032 

41: compiled :0.7281259501809544^0.9414406865385655=0.7417808233050295 
41: Strict pow :0.7281259501809544^0.9414406865385655=0.7417808233050294 

49: compiled :0.0727813262968815^0.09866028976654662=0.7721942440239148 
49: Strict pow :0.0727813262968815^0.09866028976654662=0.7721942440239149 

70: compiled :0.6574309575966407^0.759887845481148=0.7270872740201638 
70: Strict pow :0.6574309575966407^0.759887845481148=0.7270872740201637 

82: compiled :0.08662340816125613^0.4216580281197062=0.3564883826345057 
82: Strict pow :0.08662340816125613^0.4216580281197062=0.3564883826345058 

92: compiled :0.20224488115245098^0.7158182878844233=0.31851834311978916 
92: Strict pow :0.20224488115245098^0.7158182878844233=0.3185183431197892 

290 ms 

Xin lưu ý rằng Random được sử dụng để tạo các giá trị x và y, vì vậy số dặm của bạn sẽ thay đổi từ khi chạy. Nhưng tin tốt là ít nhất là kết quả của phiên bản biên dịch của Math.pow phù hợp với phiên bản được phiên dịch của Math.pow. (Tắt chủ đề: ngay cả sự nhất quán này chỉ được thực thi trong năm 2012 với một loạt các bản sửa lỗi từ phía OpenJDK.)

Lý do?

Vâng, đó là vì OpenJDK sử dụng các hàm nội tại và thời gian chạy để thực hiện Math.pow (và các hàm toán học khác), thay vì chỉ thực thi mã Java. Mục đích chính là tận dụng các hướng dẫn x87 để có thể tăng hiệu suất cho việc tính toán. Do đó, StrictMath.pow không bao giờ được gọi từ Math.pow khi chạy (đối với phiên bản OpenJDK mà chúng tôi vừa sử dụng, chính xác).

Và arragement này là hoàn toàn hợp pháp theo quy định của Javadoc của Math lớp (cũng trích dẫn bởi @coobird trên):

Lớp Math chứa phương pháp để thực hiện các hoạt động số cơ bản như tiểu mũ, logarit, căn bậc hai, và hàm lượng giác.

Không giống như một số phương pháp số của lớp StrictMath, tất cả việc triển khai các hàm tương đương của lớp Toán không được định nghĩa để trả về kết quả tương tự bit-cho-bit. Sự thư giãn này cho phép triển khai hiệu suất tốt hơn khi không yêu cầu khả năng lặp lại nghiêm ngặt.

Theo mặc định, nhiều phương thức Math chỉ cần gọi phương thức tương đương trong StrictMath để triển khai. Các trình tạo mã được khuyến khích sử dụng các thư viện gốc cụ thể cho nền tảng hoặc các hướng dẫn bộ vi xử lý, nếu có, để cung cấp các phương thức toán học hiệu suất cao hơn. Việc triển khai hiệu năng cao hơn như vậy vẫn phải phù hợp với đặc điểm kỹ thuật cho Toán học.

Và kết luận? Vâng, đối với các ngôn ngữ có tạo mã động như Java, hãy đảm bảo rằng những gì bạn thấy từ mã 'tĩnh' khớp với những gì được thực thi khi chạy. Đôi mắt của bạn đôi khi có thể thực sự đánh lừa bạn.

0

Trích dẫn java.lang.Math:

Độ chính xác của các dấu chấm động Math phương pháp được đo về ulps, đơn vị ở vị trí cuối cùng.

...

Nếu một phương pháp luôn luôn có lỗi ít hơn 0,5 ulps, phương pháp này luôn trả về số dấu chấm động gần kết quả chính xác; phương thức như vậy là được làm tròn chính xác. Một phương pháp được làm tròn một cách chính xác nói chung là tốt nhất một xấp xỉ dấu phẩy động có thể là; tuy nhiên, nó không thực tế đối với nhiều phương thức dấu phẩy động được làm tròn chính xác.

Và sau đó chúng ta thấy dưới Math.pow(..), ví dụ:

Kết quả tính toán phải nằm trong 1 ULP của kết quả chính xác.

Bây giờ, ulp là gì? Như mong đợi, java.lang.Math.ulp(1.0) cung cấp cho 2.220446049250313e-16, là 2 -52. (Ngoài ra Math.ulp(8) cho cùng một giá trị như Math.ulp(10)Math.ulp(15), nhưng không phải là Math.ulp(16).) Nói cách khác, chúng ta đang nói về bit cuối cùng của phần định trị.

Vì vậy, kết quả trả về bởi java.lang.Math.pow(..) có thể sai ở cuối cùng của 52 bit của phần định trị, như chúng ta có thể xác nhận trong câu trả lời của Tony Guan.

Sẽ tốt hơn khi đào lên một số mã ulp 1 và 0,5 ulp cụ thể để so sánh. Tôi sẽ suy đoán rằng khá nhiều công việc phụ là cần thiết để có được rằng bit cuối cùng chính xác cho cùng một lý do rằng nếu chúng ta biết hai con số A và B làm tròn đến 52 con số đáng kể và chúng tôi muốn biết A × B đúng 52 con số đáng kể , với làm tròn chính xác, sau đó thực sự chúng ta cần phải biết một vài bit phụ của A và B để có được bit cuối cùng của A × B đúng. Nhưng điều đó có nghĩa là chúng ta không nên làm tròn các kết quả trung gian A và B bằng cách buộc chúng vào số tăng gấp đôi, chúng ta cần, một loại rộng hơn cho kết quả trung gian. (Trong những gì tôi đã thấy, hầu hết việc triển khai các hàm toán học phụ thuộc rất nhiều vào các phép nhân với các hệ số được xác định trước bằng mã cứng, vì vậy nếu chúng cần rộng hơn gấp đôi, có hiệu quả lớn.)

Các vấn đề liên quan