TL; DR
- Sử dụng hàm sau thay cho giải pháp hiện được chấp nhận để tránh một số kết quả không mong muốn trong một số trường hợp giới hạn nhất định, đồng thời có khả năng hiệu quả hơn.
- Biết mức độ không chính xác dự kiến mà bạn có trên các con số của mình và cung cấp chúng cho phù hợp trong hàm so sánh.
bool nearly_equal(
float a, float b,
float epsilon = 128 * FLT_EPSILON, float relth = FLT_MIN)
{
assert(std::numeric_limits<float>::epsilon() <= epsilon);
assert(epsilon < 1.f);
if (a == b) return true;
auto diff = std::abs(a-b);
auto norm = std::min((std::abs(a) + std::abs(b)), std::numeric_limits<float>::max());
return diff < std::max(relth, epsilon * norm);
}
Đồ họa, xin vui lòng?
Khi so sánh các số dấu phẩy động, có hai "chế độ".
Chế độ đầu tiên là chế độ tương đối , trong đó sự khác biệt giữa x
và y
được coi là tương đối với biên độ của chúng |x| + |y|
. Khi vẽ đồ thị ở dạng 2D, nó đưa ra cấu hình sau, trong đó màu xanh lá cây có nghĩa là bằng x
và y
. (Tôi lấy epsilon
0,5 cho mục đích minh họa).
Chế độ tương đối là chế độ được sử dụng cho các giá trị dấu chấm động "bình thường" hoặc "đủ lớn". (Nói thêm về điều đó sau).
Chế độ thứ hai là một chế độ tuyệt đối , khi chúng ta chỉ đơn giản so sánh sự khác biệt của chúng với một số cố định. Nó đưa ra cấu hình sau (một lần nữa với epsilon
0,5 và relth
1 để minh họa).
Chế độ so sánh tuyệt đối này được sử dụng cho các giá trị dấu chấm động "nhỏ".
Bây giờ câu hỏi là, làm thế nào để chúng ta kết hợp hai mẫu phản hồi đó lại với nhau.
Trong câu trả lời của Michael Borgwardt, công tắc dựa trên giá trị của diff
, giá trị này sẽ nằm bên dưới relth
( Float.MIN_NORMAL
trong câu trả lời của anh ấy). Vùng chuyển đổi này được hiển thị như được tô trong biểu đồ bên dưới.
Bởi vì relth * epsilon
là nhỏ hơn relth
, các bản vá lỗi màu xanh lá cây không dính vào nhau, do đó cung cấp cho các giải pháp một tài sản xấu: chúng ta có thể tìm thấy ba con số như vậy x < y_1 < y_2
nhưng x == y2
nhưng x != y1
.
Lấy ví dụ nổi bật này:
x = 4.9303807e-32
y1 = 4.930381e-32
y2 = 4.9309825e-32
Chúng tôi có x < y1 < y2
, và thực tế y2 - x
là lớn hơn gấp 2000 lần y1 - x
. Và với giải pháp hiện tại,
nearlyEqual(x, y1, 1e-4) == False
nearlyEqual(x, y2, 1e-4) == True
Ngược lại, trong giải pháp được đề xuất ở trên, vùng chuyển đổi dựa trên giá trị của |x| + |y|
, được biểu thị bằng hình vuông có dấu gạch dưới bên dưới. Nó đảm bảo rằng cả hai khu vực kết nối một cách duyên dáng.
Ngoài ra, mã ở trên không có phân nhánh, có thể hiệu quả hơn. Hãy xem xét rằng các hoạt động như max
và abs
, mà ưu tiên cần phân nhánh, thường có hướng dẫn lắp ráp chuyên dụng. Vì lý do này, tôi nghĩ cách tiếp cận này vượt trội hơn so với một giải pháp khác là sửa lỗi của Michael nearlyEqual
bằng cách thay đổi công tắc từ diff < relth
sang diff < eps * relth
, sau đó sẽ tạo ra mẫu phản hồi về cơ bản giống nhau.
Chuyển đổi giữa so sánh tương đối và tuyệt đối ở đâu?
Việc chuyển đổi giữa các chế độ đó được thực hiện xung quanh relth
, được thực hiện như FLT_MIN
trong câu trả lời được chấp nhận. Lựa chọn này có nghĩa là đại diện của float32
là giới hạn độ chính xác của các số dấu phẩy động của chúng ta.
Điều này không phải lúc nào cũng có ý nghĩa. Ví dụ: nếu các số bạn so sánh là kết quả của một phép trừ, có lẽ một số thứ trong phạm vi FLT_EPSILON
có ý nghĩa hơn. Nếu chúng là căn bình phương của các số bị trừ, thì số không chính xác có thể cao hơn.
Nó là khá rõ ràng khi bạn xem xét so sánh một dấu phẩy động với 0
. Ở đây, bất kỳ so sánh tương đối nào sẽ không thành công, bởi vì |x - 0| / (|x| + 0) = 1
. Vì vậy, so sánh cần chuyển sang chế độ tuyệt đối khi x
theo thứ tự không chính xác trong tính toán của bạn - và hiếm khi nó thấp bằng FLT_MIN
.
Đây là lý do cho sự ra đời của relth
tham số trên.
Ngoài ra, bằng cách không nhân relth
với epsilon
, việc giải thích tham số này rất đơn giản và tương ứng với mức độ chính xác của số mà chúng ta mong đợi trên những con số đó.
Toán học ầm ầm
(giữ ở đây chủ yếu vì niềm vui của riêng tôi)
Nói chung hơn, tôi giả sử rằng một toán tử so sánh dấu phẩy động hoạt động tốt =~
nên có một số thuộc tính cơ bản.
Những điều sau đây là khá rõ ràng:
- tự bình đẳng:
a =~ a
- đối xứng:
a =~ b
ngụ ýb =~ a
- bất biến bởi đối lập:
a =~ b
ngụ ý-a =~ -b
(Chúng tôi không có a =~ b
và b =~ c
ngụ ý a =~ c
, =~
không phải là mối quan hệ tương đương).
Tôi sẽ thêm các thuộc tính sau cụ thể hơn để so sánh dấu phẩy động
- nếu
a < b < c
, thì a =~ c
ngụ ý a =~ b
(các giá trị gần hơn cũng phải bằng nhau)
- nếu
a, b, m >= 0
thì a =~ b
ngụ ý a + m =~ b + m
(các giá trị lớn hơn với cùng sự khác biệt cũng phải bằng nhau)
- nếu
0 <= λ < 1
thì a =~ b
ngụ ý λa =~ λb
(có lẽ ít rõ ràng hơn đối với lập luận cho).
Các thuộc tính đó đã cung cấp các ràng buộc mạnh mẽ đối với các hàm gần bằng nhau. Chức năng đề xuất ở trên xác minh chúng. Có lẽ một hoặc một số thuộc tính hiển nhiên bị thiếu.
Khi người ta nghĩ về =~
một họ quan hệ bình đẳng được =~[Ɛ,t]
tham số hóa bởi Ɛ
và relth
, người ta cũng có thể thêm
- nếu
Ɛ1 < Ɛ2
sau đó a =~[Ɛ1,t] b
ngụ ý a =~[Ɛ2,t] b
(bình đẳng đối với một mức độ khoan dung nhất định có nghĩa là bình đẳng ở mức độ khoan dung cao hơn)
- nếu
t1 < t2
sau đó a =~[Ɛ,t1] b
ngụ ý a =~[Ɛ,t2] b
(bình đẳng cho một không chính xác nhất định nghĩa là bình đẳng cho một không chính xác cao hơn)
Giải pháp được đề xuất cũng xác minh những điều này.