Có thể lấy 0 bằng cách trừ hai số dấu phẩy động không bằng nhau không?


131

Có thể lấy phép chia cho 0 (hoặc vô cực) trong ví dụ sau không?

public double calculation(double a, double b)
{
     if (a == b)
     {
         return 0;
     }
     else
     {
         return 2 / (a - b);
     }
}

Trong trường hợp bình thường, nó sẽ không, tất nhiên. Nhưng điều gì xảy ra nếu abrất gần, có thể (a-b)dẫn đến kết quả là 0do độ chính xác của phép tính?

Lưu ý rằng câu hỏi này là dành cho Java, nhưng tôi nghĩ nó sẽ áp dụng cho hầu hết các ngôn ngữ lập trình.


49
Tôi sẽ phải thử tất cả các kết hợp của đôi, mà sẽ mất một thời gian :)
Thirler

3
@Thirler nghe có vẻ như là một thời gian để sử dụng JUnit tests cho tôi!
Matt Clark

7
@bluebrain, tôi đoán là số chữ 2.000 của bạn có chứa nhiều số thập phân được biểu thị bằng số float. Vì vậy, những cái cuối cùng sẽ không được đại diện bởi số lượng sử dụng thực tế trong so sánh.
Thirler

4
@Thirler có lẽ. 'bạn thực sự không thể đảm bảo rằng số bạn gán cho số float hoặc double là chính xác'
guness 12/2/2015

4
Chỉ cần lưu ý rằng trả về 0 trong trường hợp đó có thể dẫn đến sự mơ hồ khó gỡ lỗi, vì vậy hãy đảm bảo bạn thực sự muốn trả về 0 thay vì ném ngoại lệ hoặc trả lại NaN.
m0skit0

Câu trả lời:


132

Trong Java, a - bkhông bao giờ bằng 0nếu a != b. Điều này là do Java bắt buộc các hoạt động của dấu phẩy động IEEE 754 hỗ trợ các số không chuẩn hóa. Từ thông số kỹ thuật :

Cụ thể, ngôn ngữ lập trình Java yêu cầu hỗ trợ các số dấu phẩy động không chuẩn hóa của IEEE 754 và dòng chảy dần dần, giúp dễ dàng chứng minh các thuộc tính mong muốn của các thuật toán số cụ thể. Các phép toán dấu phẩy động không "tuôn về 0" nếu kết quả tính toán là một số không chuẩn hóa.

Nếu một FPU hoạt động với các số không chuẩn hóa , trừ các số không bằng nhau không bao giờ có thể tạo ra số không (không giống như phép nhân), cũng xem câu hỏi này .

Đối với các ngôn ngữ khác, nó phụ thuộc. Trong C hoặc C ++, ví dụ, hỗ trợ IEEE 754 là tùy chọn.

Điều đó nói rằng, có thể cho biểu thức 2 / (a - b)tràn, ví dụ với a = 5e-308b = 4e-308.


4
Tuy nhiên OP muốn biết về 2 / (ab). Điều này có thể được đảm bảo là hữu hạn?
Taemyr

Cảm ơn câu trả lời, tôi đã thêm một liên kết đến wikipedia để giải thích về các số không chuẩn hóa.
Thirler 12/2/2015

3
@Taemyr Xem chỉnh sửa của tôi. Sự phân chia thực sự có thể tràn.
nwellnhof

@Taemyr (a,b) = (3,1)=> 2/(a-b) = 2/(3-1) = 2/2 = 1Điều này có đúng với điểm nổi của IEEE không, tôi không biết
Cole Johnson

1
@DrewDormann IEEE 754 cũng là tùy chọn cho C99. Xem Phụ lục F của tiêu chuẩn.
nwellnhof

50

Như một cách giải quyết, những gì sau đây?

public double calculation(double a, double b) {
     double c = a - b;
     if (c == 0)
     {
         return 0;
     }
     else
     {
         return 2 / c;
     }
}

Bằng cách đó, bạn không phụ thuộc vào hỗ trợ của IEEE trong bất kỳ ngôn ngữ nào.


6
Tránh vấn đề và đơn giản hóa tất cả các bài kiểm tra cùng một lúc. Tôi thích.
Joshua

11
-1 Nếu a=b, bạn không nên quay lại 0. Chia cho 0trong IEEE 754 giúp bạn vô cùng, không phải là một ngoại lệ. Bạn đang tránh vấn đề, vì vậy việc trở lại 0là một lỗi đang chờ xảy ra. Hãy xem xét 1/x + 1. Nếu x=0, điều đó dẫn đến 1, không phải giá trị chính xác: vô cùng.
Cole Johnson

5
@ColeJohnson câu trả lời đúng cũng không phải là vô cùng (trừ khi bạn chỉ định giới hạn đến từ bên nào, bên phải = + inf, bên trái = -inf, unspecified = không xác định hoặc NaN).
Nick T

12
@ChrisHayes: Đây là một câu trả lời hợp lệ cho câu hỏi nhận ra rằng câu hỏi có thể là vấn đề XY: meta.stackexchange.com/questions/66377/what-is-the-xy-probols
slebetman 13/2/2015

17
@ColeJohnson Trở về 0không thực sự là vấn đề. Đây là những gì OP làm trong câu hỏi. Bạn có thể đặt một ngoại lệ hoặc bất cứ điều gì phù hợp với tình huống trong phần đó của khối. Nếu bạn không thích quay lại 0, đó sẽ là một lời chỉ trích cho câu hỏi. Chắc chắn, làm như OP đã không đảm bảo một câu trả lời cho câu trả lời. Câu hỏi này không liên quan gì đến việc tính toán thêm sau khi hàm đã cho hoàn thành. Đối với tất cả những gì bạn biết, yêu cầu của chương trình bắt buộc phải trả lại 0.
jpmc26

25

Bạn sẽ không có được phép chia cho 0 bất kể giá trị là bao nhiêu a - b, vì phép chia dấu phẩy động bằng 0 không có ngoại lệ. Nó trả về vô cùng.

Bây giờ, cách duy nhất a == bsẽ trả về true là nếu abchứa các bit chính xác giống nhau. Nếu chúng khác nhau chỉ bằng một bit có ý nghĩa nhỏ nhất, sự khác biệt giữa chúng sẽ không bằng 0.

BIÊN TẬP :

Như Bathsheba bình luận chính xác, có một số trường hợp ngoại lệ:

  1. "Không phải là một số so sánh" sai với chính nó nhưng sẽ có các mẫu bit giống hệt nhau.

  2. -0.0 được định nghĩa để so sánh true với +0.0 và các mẫu bit của chúng là khác nhau.

Vì vậy, nếu cả hai abDouble.NaN, bạn sẽ đạt được mệnh đề khác, nhưng vì NaN - NaNcũng trả về NaN, bạn sẽ không được chia cho số không.


11
Eran; không đúng sự thật "Không phải là một số so sánh" sai với chính nó nhưng sẽ có các mẫu bit giống hệt nhau. Ngoài ra -0.0 được định nghĩa để so sánh true với +0.0 và các mẫu bit của chúng là khác nhau.
Bathsheba

1
@Bathsheba Tôi không xem xét những trường hợp đặc biệt này. Cảm ơn các bình luận.
Eran

2
@Eran, điểm rất hay là chia cho 0 sẽ trả về vô cực trong một điểm nổi. Đã thêm nó vào câu hỏi.
Thirler

2
@Prashant nhưng việc phân chia sẽ không diễn ra trong trường hợp này, vì a == b sẽ trả về đúng.
Eran

3
Trên thực tế, bạn có thể có một ngoại lệ FP để chia cho 0, đó là một tùy chọn được xác định theo tiêu chuẩn IEEE-754, mặc dù có lẽ đó không phải là ý nghĩa của hầu hết mọi người với "ngoại lệ";)
Voo

17

Không có trường hợp nào sự phân chia bằng 0 có thể xảy ra ở đây.

Các SMT Solver Z3 hỗ trợ IEEE chính xác số học dấu chấm. Hãy yêu cầu Z3 tìm số absao cho a != b && (a - b) == 0:

(set-info :status unknown)
(set-logic QF_FP)
(declare-fun b () (FloatingPoint 8 24))
(declare-fun a () (FloatingPoint 8 24))
(declare-fun rm () RoundingMode)
(assert
(and (not (fp.eq a b)) (fp.eq (fp.sub rm a b) +zero) true))
(check-sat)

Kết quả là UNSAT. Không có con số như vậy.

Chuỗi SMTLIB ở trên cũng cho phép Z3 chọn chế độ làm tròn tùy ý ( rm). Điều này có nghĩa là kết quả giữ cho tất cả các chế độ làm tròn có thể (trong đó có năm chế độ). Kết quả cũng bao gồm khả năng bất kỳ biến nào trong trò chơi có thể là NaNhoặc vô cùng.

a == bđược thực hiện như fp.eqchất lượng để +0f-0fso sánh bằng nhau. Việc so sánh với số không được thực hiện bằng cách sử dụng fp.eqlà tốt. Vì câu hỏi nhằm tránh sự phân chia bằng 0 nên đây là so sánh thích hợp.

Nếu xét nghiệm bình đẳng được thực hiện sử dụng bình đẳng Bitwise, +0f-0fsẽ là một cách để làm cho a - bkhông. Một phiên bản trước không chính xác của câu trả lời này chứa chi tiết chế độ về trường hợp đó cho người tò mò.

Z3 Online chưa hỗ trợ lý thuyết FPA. Kết quả này thu được bằng cách sử dụng nhánh không ổn định mới nhất. Nó có thể được sao chép bằng các ràng buộc .NET như sau:

var fpSort = context.MkFPSort32();
var aExpr = (FPExpr)context.MkConst("a", fpSort);
var bExpr = (FPExpr)context.MkConst("b", fpSort);
var rmExpr = (FPRMExpr)context.MkConst("rm", context.MkFPRoundingModeSort());
var fpZero = context.MkFP(0f, fpSort);
var subExpr = context.MkFPSub(rmExpr, aExpr, bExpr);
var constraintExpr = context.MkAnd(
        context.MkNot(context.MkFPEq(aExpr, bExpr)),
        context.MkFPEq(subExpr, fpZero),
        context.MkTrue()
    );

var smtlibString = context.BenchmarkToSMTString(null, "QF_FP", null, null, new BoolExpr[0], constraintExpr);

var solver = context.MkSimpleSolver();
solver.Assert(constraintExpr);

var status = solver.Check();
Console.WriteLine(status);

Sử dụng Z3 để trả lời câu hỏi IEEE phao là tốt đẹp vì rất khó để bỏ qua trường hợp (ví dụ như NaN, -0f, +-inf) và bạn có thể đặt câu hỏi tùy ý. Không cần phải giải thích và trích dẫn thông số kỹ thuật. Bạn thậm chí có thể hỏi các câu hỏi hỗn hợp và số nguyên như " int log2(float)thuật toán cụ thể này có đúng không?".


Bạn có thể vui lòng thêm một liên kết đến SMT Solver Z3 và một liên kết đến một trình thông dịch trực tuyến không? Trong khi câu trả lời này có vẻ hoàn toàn hợp pháp, ai đó có thể nghĩ rằng những kết quả này là sai.
AL

12

Hàm được cung cấp thực sự có thể trả về vô cực:

public class Test {
    public static double calculation(double a, double b)
    {
         if (a == b)
         {
             return 0;
         }
         else
         {
             return 2 / (a - b);
         }
    }    

    /**
     * @param args
     */
    public static void main(String[] args) {
        double d1 = Double.MIN_VALUE;
        double d2 = 2.0 * Double.MIN_VALUE;
        System.out.println("Result: " + calculation(d1, d2)); 
    }
}

Đầu ra là Result: -Infinity.

Khi kết quả của phép chia lớn đến mức được lưu trữ gấp đôi, vô cực được trả về ngay cả khi mẫu số khác không.


6

Trong triển khai điểm nổi phù hợp với IEEE-754, mỗi loại điểm nổi có thể giữ các số ở hai định dạng. Một ("chuẩn hóa") được sử dụng cho hầu hết các giá trị dấu phẩy động, nhưng số nhỏ nhất thứ hai mà nó có thể biểu thị chỉ lớn hơn một chút so với nhỏ nhất, và do đó, sự khác biệt giữa chúng không thể biểu thị theo cùng định dạng. Định dạng khác ("không chuẩn hóa") chỉ được sử dụng cho các số rất nhỏ không thể biểu thị ở định dạng đầu tiên.

Mạch để xử lý định dạng dấu phẩy động không chuẩn hóa một cách hiệu quả là tốn kém, và không phải tất cả các bộ xử lý đều bao gồm nó. Một số bộ vi xử lý đưa ra một sự lựa chọn giữa một trong hai có hoạt động trên số thực sự nhỏ có nhiều chậm hơn so với các hoạt động trên các giá trị khác, hoặc có bộ vi xử lý đơn giản coi con số đó là quá nhỏ so với định dạng bình thường như bằng không.

Các đặc tả Java ngụ ý rằng việc triển khai phải hỗ trợ định dạng không chuẩn hóa, ngay cả trên các máy mà làm như vậy sẽ khiến mã chạy chậm hơn. Mặt khác, có thể một số triển khai có thể cung cấp các tùy chọn để cho phép mã chạy nhanh hơn để đổi lấy việc xử lý các giá trị hơi cẩu thả đối với hầu hết các mục đích là quá nhỏ đối với vấn đề (trong trường hợp giá trị quá nhỏ không thành vấn đề, thì nó có thể gây khó chịu khi tính toán với chúng mất gấp mười lần so với các phép tính quan trọng, do đó, trong nhiều tình huống thực tế, từ 0 đến 0 sẽ hữu ích hơn so với số học chậm nhưng chính xác).


6

Vào thời xa xưa trước IEEE 754, hoàn toàn có thể a! = B không ngụ ý ab! = 0 và ngược lại. Đó là một trong những lý do để tạo ra IEEE 754 ngay từ đầu.

Với IEEE 754, nó gần như được đảm bảo. Trình biên dịch C hoặc C ++ được phép thực hiện một thao tác với độ chính xác cao hơn mức cần thiết. Vì vậy, nếu a và b không phải là biến mà là biểu thức, thì (a + b)! = C không ngụ ý (a + b) - c! = 0, vì a + b có thể được tính một lần với độ chính xác cao hơn và một lần không có độ chính xác cao hơn.

Nhiều FPU có thể được chuyển sang chế độ trong đó chúng không trả về số không chuẩn hóa nhưng thay thế chúng bằng 0. Trong chế độ đó, nếu a và b là các số được chuẩn hóa nhỏ trong đó chênh lệch nhỏ hơn số chuẩn hóa nhỏ nhất nhưng lớn hơn 0, a ! = b cũng không đảm bảo a == b.

"Không bao giờ so sánh số dấu phẩy động" là chương trình sùng bái hàng hóa. Trong số những người có câu thần chú "bạn cần một epsilon", hầu hết đều không biết làm thế nào để chọn epsilon đó đúng cách.


2

Tôi có thể nghĩ về một trường hợp mà bạn thể có thể gây ra điều này xảy ra. Đây là một mẫu tương tự trong cơ sở 10 - thực sự, điều này sẽ xảy ra trong cơ sở 2, tất nhiên.

Số dấu phẩy động được lưu trữ ít nhiều trong ký hiệu khoa học - nghĩa là, thay vì nhìn thấy 35.2, số được lưu trữ sẽ giống như 3,52e2.

Hãy tưởng tượng để thuận tiện cho việc chúng ta có một đơn vị dấu phẩy động hoạt động trong cơ sở 10 và có 3 chữ số chính xác. Điều gì xảy ra khi bạn trừ 9,99 từ 10,0?

1,00e2-9,99e1

Thay đổi để cung cấp cho mỗi giá trị cùng một số mũ

1,00e2-0,999e2

Làm tròn đến 3 chữ số

1,00e2-1,00e2

À!

Liệu điều này có thể xảy ra hay không phụ thuộc vào thiết kế của FPU. Vì phạm vi của số mũ cho một nhân đôi là rất lớn, phần cứng phải làm tròn nội bộ tại một số điểm, nhưng trong trường hợp trên, chỉ cần thêm 1 chữ số bên trong sẽ ngăn chặn mọi vấn đề.


1
Các thanh ghi giữ các toán hạng được căn chỉnh để trừ được yêu cầu giữ thêm hai bit, được gọi là "bit bảo vệ", để xử lý tình huống này. Trong trường hợp phép trừ sẽ gây ra một khoản vay từ bit có ý nghĩa nhất, cường độ của toán hạng nhỏ hơn phải vượt quá một nửa so với toán hạng lớn hơn (ngụ ý rằng nó chỉ có thể có thêm một bit chính xác) hoặc nếu không thì kết quả phải ít nhất là một nửa độ lớn của toán hạng nhỏ hơn (ngụ ý rằng nó sẽ chỉ cần thêm một bit, cộng với thông tin đủ để đảm bảo làm tròn chính xác).
supercat

1
Cho dù điều này có thể xảy ra hay không phụ thuộc vào thiết kế của FPU, Không, điều đó không thể xảy ra vì định nghĩa Java nói rằng điều đó không thể. Thiết kế của FPU không có gì để làm với nó.
Pascal Cuoq

@PascalCuoq: Sửa lỗi cho tôi nếu tôi sai, nhưng strictfpkhông được bật, các phép tính có thể mang lại các giá trị quá nhỏ doublenhưng sẽ phù hợp với giá trị dấu phẩy động có độ chính xác mở rộng.
supercat

@supercat Sự vắng mặt của strictfpchỉ ảnh hưởng đến các giá trị của các kết quả trung gian của Cameron , và tôi đang trích dẫn từ docs.oracle.com/javase/specs/jls/se7/html/jls-15.html#jls-15.4 . abdoublecác biến, không phải là kết quả trung gian, vì vậy giá trị của chúng là giá trị chính xác kép, do đó là bội số của 2 ^ -1074. Do đó, phép trừ của hai giá trị độ chính xác kép này là bội số của 2 ^ -1074, vì vậy phạm vi số mũ rộng hơn sẽ thay đổi thuộc tính rằng chênh lệch là 0 iff a == b.
Pascal Cuoq

@supercat Điều này có ý nghĩa - bạn chỉ cần thêm một chút để làm điều này.
Keldor314

1

Bạn không bao giờ nên so sánh phao hoặc đôi cho bình đẳng; bởi vì, bạn không thể thực sự đảm bảo rằng số bạn gán cho số float hoặc double là chính xác.

Để so sánh số float cho bình đẳng hoàn toàn, bạn cần kiểm tra xem giá trị có "đủ gần" với cùng một giá trị không:

if ((first >= second - error) || (first <= second + error)

6
"Không bao giờ" là một chút mạnh mẽ, nhưng nói chung đây là lời khuyên tốt.
Mark Pattison

1
Trong khi bạn đúng, abs(first - second) < error(hoặc <= error) dễ dàng và súc tích hơn.
glglgl

3
Mặc dù đúng trong hầu hết các trường hợp ( không phải tất cả ), nhưng không thực sự trả lời câu hỏi.
milleniumorms

4
Kiểm tra số dấu phẩy động cho đẳng thức khá thường xuyên hữu ích. Không có gì lành mạnh khi so sánh với một epsilon chưa được lựa chọn cẩn thận, và thậm chí ít lành mạnh hơn về việc so sánh với một epsilon khi một người đang kiểm tra sự bình đẳng.
tmyklebu

1
Nếu bạn sắp xếp một mảng trên khóa dấu phẩy động, tôi có thể đảm bảo rằng mã của bạn sẽ không hoạt động nếu bạn cố gắng sử dụng các thủ thuật so sánh các số dấu phẩy động với một epsilon. Bởi vì đảm bảo rằng a == b và b == c ngụ ý a == c không còn nữa. Đối với bảng băm, vấn đề chính xác tương tự. Khi bình đẳng không mang tính bắc cầu, thuật toán của bạn sẽ bị hỏng.
gnasher729

1

Chia cho số 0 là không xác định, vì giới hạn từ số dương có xu hướng vô cùng, giới hạn từ số âm có xu hướng đến vô cực âm.

Không chắc đây là C ++ hay Java vì không có thẻ ngôn ngữ.

double calculation(double a, double b)
{
     if (a == b)
     {
         return nan(""); // C++

         return Double.NaN; // Java
     }
     else
     {
         return 2 / (a - b);
     }
}

1

Vấn đề cốt lõi là đại diện máy tính của một số kép (còn gọi là số thực hoặc số thực trong ngôn ngữ toán học) là sai khi bạn có số thập phân "quá nhiều", ví dụ như khi bạn xử lý gấp đôi không thể được viết dưới dạng giá trị số ( pi hoặc kết quả của 1/3).

Vì vậy, a == b không thể được thực hiện với bất kỳ giá trị kép nào của a và b, làm thế nào để bạn đối phó với a == b khi a = 0.333 và b = 1/3? Tùy thuộc vào hệ điều hành của bạn so với FPU so với số so với ngôn ngữ so với số 3 sau 0, bạn sẽ có đúng hoặc sai.

Dù sao, nếu bạn thực hiện "tính toán giá trị kép" trên máy tính, bạn phải đối phó với độ chính xác, vì vậy thay vì làm a==b, bạn phải làm absolute_value(a-b)<epsilonvà epsilon liên quan đến những gì bạn đang lập mô hình tại thời điểm đó trong thuật toán của bạn. Bạn không thể có một giá trị epsilon cho tất cả các so sánh kép của bạn.

Tóm lại, khi bạn nhập a == b, bạn có một biểu thức toán học không thể dịch trên máy tính (đối với bất kỳ số dấu phẩy động nào).

PS: hum, mọi thứ tôi trả lời ở đây ít nhiều trong những phản hồi và bình luận của người khác.


1

Dựa trên phản hồi của @malarres và nhận xét @Taemyr, đây là đóng góp nhỏ của tôi:

public double calculation(double a, double b)
{
     double c = 2 / (a - b);

     // Should not have a big cost.
     if (isnan(c) || isinf(c))
     {
         return 0; // A 'whatever' value.
     }
     else
     {
         return c;
     }
}

Quan điểm của tôi là nói: cách dễ nhất để biết kết quả của phép chia là nan hay inf là thực tế để thực hiện phép chia.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.