2012-02-03 26 views
12

Tôi muốn xác định (trong C++) nếu một số phao là nghịch đảo nhân của một số phao khác. Vấn đề là tôi phải sử dụng một biến thứ ba để làm điều đó. Ví dụ mã này:Làm thế nào để kiểm tra phụ thuộc của phao nổi

float x=5,y=0.2; 
if(x==(1/y)) cout<<"They are the multiplicative inverse of eachother"<<endl; 
else cout<<"They are NOT the multiplicative inverse of eachother"<<endl; 

chí đầu ra: "họ không ...", đó là sai lầm và mã này:

float x=5,y=0.2,z; 
z=1/y; 
if(x==z) cout<<"They are the multiplicative inverse of eachother"<<endl; 
else cout<<"They are NOT the multiplicative inverse of eachother"<<endl; 

chí đầu ra: "họ là ...", mà là đúng .
tại sao điều này lại xảy ra?

+7

http://docs.oracle.com/ cd/E19957-01/806-3568/ncg_goldberg.html – Mysticial

+3

'((x * y) == 1)' không hoạt động? – Vyktor

+0

Tôi đã thêm một số thông tin trong câu trả lời của mình. Và +1 cho một câu hỏi hiệu quả. – Gangnus

Trả lời

36

Các phao chính xác vấn đề

    Bạn có hai vấn đề ở đây, nhưng cả hai đều xuất phát từ cùng một gốc

Bạn không thể so sánh nổi một cách chính xác. Bạn không thể trừ hoặc chia chúng một cách chính xác. Bạn không thể đếm được mọi thứ cho chính xác. Bất kỳ hoạt động với họ có thể (và hầu như luôn luôn) mang lại một số lỗi vào kết quả. Thậm chí a=0.2f không phải là một hoạt động chính xác. Những lý do sâu sắc hơn được giải thích rất rõ bởi các tác giả của các câu trả lời khác ở đây. (Cảm ơn của tôi và phiếu bầu cho họ cho điều đó.)

Ở đây có lỗi đầu tiên và đơn giản hơn của bạn. Bạn sẽ không bao giờ, không bao giờ , bao giờ, bao giờ, KHÔNG BAO GIỜ sử dụng trên chúng == hoặc tương đương trong ngôn ngữ bất kỳ.

Thay vì a==b, hãy sử dụng Abs(a-b)<HighestPossibleError để thay thế.


    Nhưng đây không phải là vấn đề duy nhất trong nhiệm vụ của mình.

Abs(1/y-x)<HighestPossibleError cũng không hoạt động. Ít nhất, nó sẽ không hoạt động thường xuyên. Tại sao?

Hãy lấy cặp x = 1000 và y = 0,001. Hãy lấy lỗi "bắt đầu" tương đối của y cho 10 -6.

(Lỗi tương đối = lỗi/giá trị).

Lỗi tương đối của các giá trị được thêm vào lúc nhân và chia.

1/y là khoảng 1000. Lỗi tương đối của nó giống nhau 10 -6. ("1" không có lỗi)

Điều đó làm cho lỗi tuyệt đối = 1000 * 10 -6 = 0,001. Khi bạn trừ x sau, lỗi đó sẽ là tất cả những gì còn lại. (Lỗi tuyệt đối được thêm vào lúc cộng và trừ, và lỗi của x là không đáng kể nhỏ.) Chắc chắn, bạn không tính đến những lỗi quá lớn, HighestPossibleError chắc chắn sẽ được đặt thấp hơn và chương trình của bạn sẽ ném ra một cặp x, y

Vì vậy, hai quy tắc tiếp theo cho hoạt động nổi: cố gắng không chia valuer bởi ít hơn một và Thiên Chúa giúp bạn tiết kiệm từ trừ các giá trị gần sau đó.

Có hai cách đơn giản để thoát khỏi vấn đề này.

  • Bằng sáng lập những gì của x, y có giá trị lớn hơn abs và chia 1 bởi một càng lớn và chỉ sau đó trừ một trong những ít.

  • Nếu bạn muốn so sánh 1/y against x, trong khi bạn đang làm việc nhưng với chữ cái, không giá trị, và hoạt động của bạn làm cho không có lỗi, nhân bên cả hai so sánh bằng y và bạn có 1 against x*y. (Thông thường bạn nên kiểm tra các dấu hiệu trong hoạt động đó, nhưng ở đây chúng tôi sử dụng giá trị abs, vì vậy, nó là sạch.) Kết quả so sánh không có sự phân chia nào cả.

Trong một cách ngắn:

1/y V x <=> y*(1/y) V x*y <=> 1 V x*y 

Chúng tôi đã biết rằng so sánh như 1 against x*y nên được thực hiện như vậy:

const float HighestPossibleError=1e-10; 
if(Abs(x*y-1.0)<HighestPossibleError){... 

Đó là tất cả.


P.S. Nếu bạn thực sự cần nó tất cả trên cùng một dòng, sử dụng:

if(Abs(x*y-1.0)<1e-10){... 

Nhưng đó là phong cách xấu. Tôi sẽ không tư vấn cho nó.

P.P.S. Trong ví dụ thứ hai của bạn trình biên dịch tối ưu hóa mã như vậy, rằng nó đặt z đến 5 trước khi chạy bất kỳ mã nào. Vì vậy, kiểm tra 5 chống lại 5 hoạt động ngay cả đối với phao nổi.

5

Bạn sẽ phải xác định chính xác ý nghĩa của hai lần xấp xỉ là phép nghịch đảo nhân. Nếu không, bạn sẽ không biết bạn đang thử nghiệm cái gì.

0.2 không có đại diện nhị phân chính xác. Nếu bạn lưu trữ các số không có biểu diễn chính xác với độ chính xác giới hạn, bạn sẽ không nhận được câu trả lời chính xác.

Điều tương tự cũng xảy ra theo số thập phân. Ví dụ: 1/3 không có đại diện thập phân chính xác. Bạn có thể lưu nó dưới dạng .333333. Nhưng sau đó bạn có một vấn đề. Có phải 3.333333 inverses nhân? Nếu bạn nhân chúng, bạn sẽ nhận được .999999. Nếu bạn muốn câu trả lời là "có", bạn sẽ phải tạo một phép thử cho các phép nhân nhân mà không đơn giản như nhân và kiểm tra sự bình đẳng với 1.

Điều tương tự cũng xảy ra với nhị phân.

13

Vấn đề là 0.2 không thể được đại diện chính xác trong hệ nhị phân, vì mở rộng nhị phân của nó có vô số chữ số:

1/5: 0.0011001100110011001100110011001100110011... 

này cũng tương tự như cách 1/3 không thể được đại diện chính xác trong hệ thập phân. Kể từ x được lưu trữ trong một float trong đó có một số hữu hạn các bit, các chữ số sẽ được cắt đứt tại một số điểm, ví dụ:

x: 0.0011001100110011001100110011001 

vấn đề nảy sinh vì CPU thường sử dụng một độ chính xác cao hơn trong nội bộ, vì vậy khi bạn đã chỉ tính 1/y, kết quả sẽ có nhiều chữ số hơn và khi bạn tải x để so sánh chúng, x sẽ được mở rộng để phù hợp với độ chính xác nội bộ của CPU.

1/y: 0.0011001100110011001100110011001100110011001100110011 
    x: 0.0011001100110011001100110011001000000000000000000000 

Vì vậy, khi bạn so sánh từng bit trực tiếp, chúng khác nhau.

Trong ví dụ thứ hai của bạn, tuy nhiên, lưu trữ kết quả vào một biến có nghĩa là nó bị cắt ngắn trước khi thực hiện việc so sánh, vì vậy so sánh chúng một cách chính xác này, họ đang bằng nhau:

x: 0.0011001100110011001100110011001 
    z: 0.0011001100110011001100110011001 

Nhiều trình biên dịch có công tắc bạn có thể cho phép buộc các giá trị trung gian bị cắt bớt ở mọi bước để nhất quán, tuy nhiên lời khuyên thông thường là tránh so sánh trực tiếp giữa các giá trị dấu phẩy động và thay vào đó kiểm tra xem chúng có khác nhau ít hơn một chút giá trị epsilon không, đó là những gì Gangnus is suggesting.

0

Điều đáng chú ý là bất kể quy tắc làm tròn là gì, bạn mong đợi kết quả của hai phiên bản giống nhau (hai lần sai hoặc hai lần đúng)!

Có lẽ, trong trường hợp đầu tiên quảng cáo có độ chính xác cao hơn trong thanh ghi FPU diễn ra khi đánh giá x == 1/y, trong khi z = 1/y thực sự lưu trữ kết quả có độ chính xác đơn.

Những người đóng góp khác đã giải thích lý do tại sao 5 == 1/0,2 có thể không thành công, tôi không cần lặp lại điều đó.

2

Các cuộc thảo luận trong các thư trả lời khác rất tuyệt và vì vậy tôi sẽ không lặp lại bất kỳ câu nào trong số họ, nhưng không có mã. Dưới đây là một chút mã để thực sự kiểm tra xem một cặp float có chính xác 1.0 khi nhân lên hay không.

Mã này tạo ra một vài giả định/khẳng định (mà thường gặp trên nền tảng x86):
- float 's là 32-bit nhị phân (AKA single precision) IEEE-754
- hoặc int' s hoặc long 's là 32-bit (tôi quyết định không dựa vào sự sẵn có của uint32_t)
- memcpy() bản nổi để ints/chờ đợi như vậy 8873283.0f trở thành 0x4B076543 (tức là chắc chắn "endianness" dự kiến)

Một thêm giả định là thế này :
- nó nhận được các phao thực tế mà * sẽ nhân lên (tức lànhân phao sẽ không sử dụng các giá trị chính xác cao hơn mà các phần cứng toán/thư viện có thể sử dụng trong nội bộ)

#include <stdio.h> 
#include <string.h> 
#include <limits.h> 
#include <assert.h> 

#define C_ASSERT(expr) extern char CAssertExtern[(expr)?1:-1] 

#if UINT_MAX >= 0xFFFFFFFF 
typedef unsigned int uint32; 
#else 
typedef unsigned long uint32; 
#endif 
typedef unsigned long long uint64; 

C_ASSERT(CHAR_BIT == 8); 
C_ASSERT(sizeof(uint32) == 4); 
C_ASSERT(sizeof(float) == 4); 

int ProductIsOne(float f1, float f2) 
{ 
    uint32 m1, m2; 
    int e1, e2, s1, s2; 
    int e; 
    uint64 m; 

    // Make sure floats are 32-bit IEE754 and 
    // reinterpreted as integers as we expect 
    { 
    static const float testf = 8873283.0f; 
    uint32 testi; 
    memcpy(&testi, &testf, sizeof(testf)); 
    assert(testi == 0x4B076543); 
    } 

    memcpy(&m1, &f1, sizeof(f1)); 
    s1 = m1 >= 0x80000000; 
    m1 &= 0x7FFFFFFF; 
    e1 = m1 >> 23; 
    m1 &= 0x7FFFFF; 
    if (e1 > 0) m1 |= 0x800000; 

    memcpy(&m2, &f2, sizeof(f2)); 
    s2 = m2 >= 0x80000000; 
    m2 &= 0x7FFFFFFF; 
    e2 = m2 >> 23; 
    m2 &= 0x7FFFFF; 
    if (e2 > 0) m2 |= 0x800000; 

    if (e1 == 0xFF || e2 == 0xFF || s1 != s2) // Inf, NaN, different signs 
    return 0; 

    m = (uint64)m1 * m2; 

    if (!m || (m & (m - 1))) // not a power of 2 
    return 0; 

    e = e1 + !e1 - 0x7F - 23 + e2 + !e2 - 0x7F - 23; 
    while (m > 1) m >>= 1, e++; 

    return e == 0; 
} 

const float testData[][2] = 
{ 
    { .1f, 10.0f }, 
    { 0.5f, 2.0f }, 
    { 0.25f, 2.0f }, 
    { 4.0f, 0.25f }, 
    { 0.33333333f, 3.0f }, 
    { 0.00000762939453125f, 131072.0f }, // 2^-17 * 2^17 
    { 1.26765060022822940E30f, 7.88860905221011805E-31f }, // 2^100 * 2^-100 
    { 5.87747175411143754E-39f, 1.70141183460469232E38f }, // 2^-127 (denormalized) * 2^127 
}; 

int main(void) 
{ 
    int i; 
    for (i = 0; i < sizeof(testData)/sizeof(testData[0]); i++) 
    printf("%g * %g %c= 1\n", 
      testData[i][0], testData[i][1], 
      "!="[ProductIsOne(testData[i][0], testData[i][1])]); 
    return 0; 
} 

Output (xem tại ideone.com):

0.1 * 10 != 1 
0.5 * 2 == 1 
0.25 * 2 != 1 
4 * 0.25 == 1 
0.333333 * 3 != 1 
7.62939e-06 * 131072 == 1 
1.26765e+30 * 7.88861e-31 == 1 
5.87747e-39 * 1.70141e+38 == 1 
+0

+1. Vì vậy, các phân số nhị phân là chính xác ở đó. Bạn chưa thử 2^(- 100) * 2^(+ 100)? – Gangnus

+0

@Gangnus: Chắc chắn, nếu đó là nhị phân, quyền hạn của 2 là chính xác. Xem [mã được cập nhật trên ideone] (http://ideone.com/o7Hpq). Chúng tôi thậm chí không cần tất cả các chữ số có nghĩa là 2^100 hoặc 2^-100 trong thập phân. –

+0

Tôi có nghĩa là, trên một số quyền lực sẽ có vấn đề để đặt sức mạnh của 2 vào phần secont của phao. – Gangnus

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