Có an toàn để kiểm tra các giá trị dấu chấm động cho bằng 0 không?


100

Tôi biết bạn không thể dựa vào bình đẳng giữa các giá trị kiểu kép hoặc thập phân một cách bình thường, nhưng tôi tự hỏi liệu 0 có phải là trường hợp đặc biệt hay không.

Mặc dù tôi có thể hiểu được sự thiếu chính xác giữa 0,00000000000001 và 0,00000000000002, nhưng bản thân 0 dường như khá khó để làm rối vì nó chỉ là con số không. Nếu bạn không chính xác vào không có gì, nó không còn là gì nữa.

Nhưng tôi không biết nhiều về chủ đề này nên không phải để tôi nói.

double x = 0.0;
return (x == 0.0) ? true : false;

Điều đó sẽ luôn luôn trở thành sự thật?


69
Các nhà điều hành ternary là dư thừa trong mã mà :)
Joel Coehoorn

5
LOL bạn đã đúng. Đi với tôi
Gene Roberts

Tôi sẽ không làm điều đó bởi vì bạn không biết, làm thế nào x được đặt thành 0. Nếu bạn vẫn muốn làm điều đó, bạn có thể muốn làm tròn hoặc tầng x để loại bỏ 1e-12 hoặc tương tự có thể được gắn thẻ ở cuối.
Rex Logan

Câu trả lời:


115

Có thể an toàn khi kỳ vọng rằng phép so sánh sẽ trả về truenếu và chỉ khi biến kép có giá trị chính xác 0.0(tất nhiên là trong đoạn mã gốc của bạn). Điều này phù hợp với ngữ nghĩa của ==toán tử. a == bcó nghĩa là " abằng với b".

Sẽ không an toàn (vì nó không chính xác ) khi kỳ vọng rằng kết quả của một phép tính nào đó sẽ bằng 0 trong số học kép (hay nói chung hơn là dấu phẩy động) bất cứ khi nào kết quả của cùng một phép tính trong Toán học thuần túy bằng 0. Điều này là do khi các phép tính đi vào nền tảng, lỗi chính xác dấu phẩy động xuất hiện - một khái niệm không tồn tại trong số học Số thực trong Toán học.


51

Nếu bạn cần thực hiện nhiều phép so sánh "bình đẳng", bạn có thể viết một hàm trợ giúp nhỏ hoặc phương thức mở rộng trong .NET 3.5 để so sánh:

public static bool AlmostEquals(this double double1, double double2, double precision)
{
    return (Math.Abs(double1 - double2) <= precision);
}

Điều này có thể được sử dụng theo cách sau:

double d1 = 10.0 * .1;
bool equals = d1.AlmostEquals(0.0, 0.0000001);

4
Bạn có thể gặp lỗi hủy trừ khi so sánh double1 và double2, trong trường hợp các số này có giá trị rất gần nhau. Tôi sẽ loại bỏ các Math.Abs và kiểm tra từng ngành riêng d1> = d2 - e và d1 <= d2 + e
Theodore Zographos

"Vì Epsilon xác định biểu thức tối thiểu của một giá trị dương có phạm vi gần bằng 0, biên độ chênh lệch giữa hai giá trị tương tự phải lớn hơn Epsilon. Thông thường, nó lớn hơn Epsilon nhiều lần. Do đó, chúng tôi khuyên bạn nên làm không sử dụng Epsilon khi so sánh các giá trị Double cho bằng nhau. " - msdn.microsoft.com/en-gb/library/ya2zha7s(v=vs.110).aspx
Rafael Costa

15

Đối với mẫu đơn giản của bạn, thử nghiệm đó là được. Nhưng những gì về điều này:

bool b = ( 10.0 * .1 - 1.0 == 0.0 );

Hãy nhớ rằng .1 là một số thập phân lặp lại trong hệ nhị phân và không thể được biểu diễn chính xác. Sau đó so sánh với mã này:

double d1 = 10.0 * .1; // make sure the compiler hasn't optimized the .1 issue away
bool b = ( d1 - 1.0 == 0.0 );

Tôi sẽ để bạn chạy thử nghiệm để xem kết quả thực tế: nhiều khả năng bạn sẽ nhớ nó theo cách đó.


5
Trên thực tế, điều này trả về true vì một số lý do (ít nhất là trong LINQPad).
Alexey Romanov

"Vấn đề .1" bạn nói về là gì?
Teejay

14

Từ mục nhập MSDN cho Double.Equals :

Độ chính xác trong so sánh

Phương pháp Equals nên được sử dụng một cách thận trọng, vì hai giá trị rõ ràng là tương đương có thể không bằng nhau do độ chính xác của hai giá trị khác nhau. Ví dụ sau báo cáo rằng giá trị Double .3333 và Double được trả về bằng cách chia 1 cho 3 là không bằng nhau.

...

Thay vì so sánh để bình đẳng, một kỹ thuật được đề xuất liên quan đến việc xác định biên độ chênh lệch có thể chấp nhận được giữa hai giá trị (chẳng hạn như 0,01% của một trong các giá trị). Nếu giá trị tuyệt đối của chênh lệch giữa hai giá trị nhỏ hơn hoặc bằng biên độ đó, thì sự khác biệt có thể là do sự khác biệt về độ chính xác và do đó, các giá trị có thể bằng nhau. Ví dụ sau sử dụng kỹ thuật này để so sánh .33333 và 1/3, hai giá trị Double mà ví dụ mã trước đó phát hiện là không bằng nhau.

Ngoài ra, hãy xem Double.Epsilon .


1
Các giá trị không hoàn toàn tương đương cũng có thể được so sánh bằng nhau. Một hy vọng rằng nếu x.Equals(y), sau đó (1/x).Equals(1/y), nhưng đó không phải là trường hợp nếu x0y1/Double.NegativeInfinity. Các giá trị đó tuyên bố là bằng nhau, mặc dù các giá trị tương hỗ của chúng thì không.
supercat

@supercat: Chúng tương đương nhau. Và họ không có đi có lại. Bạn có thể chạy lại thử nghiệm của mình với x = 0y = 0và bạn vẫn thấy điều đó 1/x != 1/y.
Ben Voigt

@BenVoigt: Với xydưới dạng double? Làm thế nào để bạn so sánh các kết quả để làm cho chúng báo cáo không bằng nhau? Lưu ý rằng 1 / 0.0 không phải là NaN.
supercat

@supercat: Ok, đó là một trong những điều mà IEEE-754 bị sai. (Đầu tiên, đó 1.0/0.0không phải là NaN như lẽ ra, vì giới hạn không phải là duy nhất. Thứ hai, các số vô hạn đó so sánh bằng nhau, mà không cần chú ý đến độ vô cực)
Ben Voigt

@BenVoigt: Nếu số 0 là kết quả của phép nhân hai số rất nhỏ, thì chia 1,0 cho số đó sẽ mang lại giá trị so sánh lớn hơn bất kỳ số nào trong số các số nhỏ có cùng dấu và nhỏ hơn bất kỳ số nào nếu một trong các số nhỏ các con số có dấu hiệu ngược lại. IMHO, IEEE-754 sẽ tốt hơn nếu nó có số 0 không dấu, nhưng là các số cực âm dương và âm.
supercat

6

Vấn đề xảy ra khi bạn so sánh các kiểu triển khai giá trị dấu chấm động khác nhau, ví dụ như so sánh float với double. Nhưng với cùng một loại, nó không phải là một vấn đề.

float f = 0.1F;
bool b1 = (f == 0.1); //returns false
bool b2 = (f == 0.1F); //returns true

Vấn đề là, lập trình viên đôi khi quên rằng kiểu ép kiểu ngầm (double to float) đang diễn ra để so sánh và kết quả là nó dẫn đến lỗi.


3

Nếu số được gán trực tiếp cho float hoặc double thì có thể an toàn để kiểm tra đối chiếu với số 0 hoặc bất kỳ số nguyên nào có thể được biểu diễn bằng 53 bit đối với số kép hoặc 24 bit đối với số thực.

Hay nói một cách khác, bạn luôn có thể gán và giá trị số nguyên cho một đôi và sau đó so sánh đôi trở lại với cùng một số nguyên và được đảm bảo rằng nó sẽ bằng nhau.

Bạn cũng có thể bắt đầu bằng cách gán một số nguyên và các phép so sánh đơn giản vẫn tiếp tục hoạt động bằng cách gắn bó với việc cộng, trừ hoặc nhân với các số nguyên (giả sử kết quả nhỏ hơn 24 bit đối với số thực là 53 bit đối với số kép). Vì vậy, bạn có thể coi float và double là số nguyên trong các điều kiện được kiểm soát nhất định.


Tôi đồng ý với tuyên bố của bạn nói chung (và ủng hộ nó) nhưng tôi tin rằng nó thực sự phụ thuộc vào việc triển khai dấu chấm động IEEE 754 có được sử dụng hay không. Và tôi tin rằng mọi máy tính "hiện đại" đều sử dụng IEEE 754, ít nhất là để lưu trữ các phao (có những quy tắc làm tròn kỳ lạ khác nhau).
Mark Lakata

2

Không, nó không ổn. Cái gọi là giá trị không chuẩn hóa (dưới chuẩn), khi được so sánh bằng 0,0, sẽ được so sánh là sai (khác 0), nhưng khi được sử dụng trong một phương trình sẽ được chuẩn hóa (trở thành 0,0). Vì vậy, sử dụng điều này như một cơ chế để tránh chia cho 0 là không an toàn. Thay vào đó, hãy thêm 1,0 và so sánh với 1,0. Điều này sẽ đảm bảo rằng tất cả các subnormal được coi là 0.


Subnormals còn được gọi là denormals
Manuel

Các subnormals không trở thành bằng 0 khi được sử dụng, mặc dù chúng có thể tạo ra kết quả giống nhau hoặc không tùy thuộc vào thao tác chính xác.
wnoise

-2

Hãy thử điều này, và bạn sẽ thấy rằng == không đáng tin cậy cho double / float.
double d = 0.1 + 0.2; bool b = d == 0.3;

Đây là câu trả lời từ Quora.


-4

Trên thực tế, tôi nghĩ tốt hơn là sử dụng các mã sau để so sánh giá trị kép với 0,0:

double x = 0.0;
return (Math.Abs(x) < double.Epsilon) ? true : false;

Tương tự cho float:

float x = 0.0f;
return (Math.Abs(x) < float.Epsilon) ? true : false;

5
Không. Từ các tài liệu từ double.Epsilon: "Nếu bạn tạo một thuật toán tùy chỉnh để xác định xem hai số dấu phẩy động có thể được coi là bằng nhau hay không, bạn phải sử dụng giá trị lớn hơn hằng số Epsilon để thiết lập biên độ chênh lệch tuyệt đối có thể chấp nhận được để hai giá trị được coi là bằng nhau. (Thông thường, biên độ chênh lệch đó lớn hơn Epsilon nhiều lần.) "
Alastair Maw

1
@AlastairMaw điều này áp dụng để kiểm tra hai bộ đôi có kích thước bất kỳ xem có bằng nhau không. Để kiểm tra bằng 0, double.Epsilon là tốt.
jwg

4
Không, không phải . Rất có thể giá trị bạn đã đạt được thông qua một phép tính nào đó chênh lệch nhiều lần so với 0, nhưng vẫn nên được coi là 0. Bạn không đạt được độ chính xác cao một cách thần kỳ trong kết quả trung gian của mình từ đâu đó, chỉ vì nó tình cờ gần bằng không.
Alastair Maw

4
Ví dụ: (1.0 / 5.0 + 1.0 / 5.0 - 1.0 / 10.0 - 1.0 / 10.0 - 1.0 / 10.0 - 1.0 / 10.0) <double.Epsilon == false (và đáng kể là về độ lớn: 2,78E-17 so với 4,94E -324)
Alastair Maw

vì vậy, độ chính xác được khuyến nghị là bao nhiêu, nếu double.Epsilon không ổn? 10 lần epsilon có ổn không? 100 lần?
liang
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.