Những đặc điểm không thực của loại thực, hoặc Hãy cẩn thận với THẬT

Sau khi xuất bản Điều về các tính năng gõ trong PostgreSQL, nhận xét đầu tiên là về những khó khăn khi làm việc với số thực. Tôi quyết định xem nhanh mã của các truy vấn SQL có sẵn để xem tần suất chúng sử dụng loại REAL. Hóa ra nó được sử dụng khá thường xuyên và không phải lúc nào các nhà phát triển cũng hiểu được những nguy hiểm đằng sau nó. Và điều này bất chấp thực tế là có khá nhiều bài viết hay trên Internet và trên Habré về các tính năng lưu trữ số thực trong bộ nhớ máy tính và cách làm việc với chúng. Do đó, trong bài viết này, tôi sẽ cố gắng áp dụng các tính năng như vậy cho PostgreSQL và sẽ cố gắng xem nhanh các vấn đề liên quan đến chúng để các nhà phát triển truy vấn SQL có thể tránh chúng dễ dàng hơn.

Tài liệu PostgreSQL nêu ngắn gọn: “Việc quản lý các lỗi như vậy và sự lan truyền của chúng trong quá trình tính toán là chủ đề của toàn bộ ngành toán học và khoa học máy tính và không được đề cập ở đây” (đồng thời giới thiệu cho người đọc một cách khôn ngoan về tiêu chuẩn IEEE 754). Những loại lỗi có nghĩa là gì ở đây? Hãy thảo luận theo thứ tự và sẽ sớm hiểu rõ lý do tại sao tôi lại cầm bút.

Hãy lấy ví dụ một yêu cầu đơn giản:

********* ЗАПРОС *********
SELECT 0.1::REAL;
**************************
float4
--------
    0.1
(1 строка)

Kết quả là chúng ta sẽ không thấy điều gì đặc biệt – chúng ta sẽ nhận được 0.1 như mong đợi. Nhưng bây giờ hãy so sánh nó với 0.1:

********* ЗАПРОС *********
SELECT 0.1::REAL = 0.1;
**************************
?column?
----------
f
(1 строка)

Không công bằng! Thật là kỳ diệu! Nhưng xa hơn, nhiều hơn nữa. Ai đó sẽ nói, tôi biết REAL hoạt động không tốt với phân số, vì vậy tôi sẽ nhập số nguyên vào đó và mọi thứ chắc chắn sẽ ổn với chúng. Ok, hãy chuyển số 123 thành REAL:

********* ЗАПРОС *********
SELECT 123456789::REAL::INT;
**************************
   int4   
-----------
123456792
(1 строка)

Và hóa ra là thêm 3 nữa! Thế là xong, cơ sở dữ liệu cuối cùng đã quên cách đếm! Hay chúng ta đang hiểu lầm điều gì đó? Hãy tìm ra nó.

Đầu tiên, chúng ta hãy nhớ đến vật chất. Như bạn đã biết, bất kỳ số thập phân nào cũng có thể được mở rộng thành lũy thừa của mười. Vì vậy, số 123.456 sẽ bằng 1*102 + 2*101 + 3*100 + 4*10-1 + 5*10-2 + ​​​​6*10-3. Nhưng máy tính hoạt động với các số ở dạng nhị phân, do đó chúng phải được biểu diễn dưới dạng khai triển lũy thừa hai. Do đó, số 5.625 trong hệ nhị phân được biểu thị là 101.101 và sẽ bằng 1*22 + 0*21 + 1*20 + 1*2-1 + 0*2-2 + 1*2-3. Và nếu lũy thừa dương của hai luôn cho số thập phân nguyên (1, 2, 4, 8, 16, v.v.), thì với số âm, mọi thứ phức tạp hơn (0.5, 0.25, 0.125, 0,0625, v.v.). Vấn đề là ở đó Không phải mọi số thập phân đều có thể được biểu diễn dưới dạng phân số nhị phân hữu hạn. Do đó, 0.1 khét tiếng của chúng ta ở dạng phân số nhị phân xuất hiện dưới dạng giá trị tuần hoàn 0.0 (0011). Do đó, giá trị cuối cùng của số này trong bộ nhớ máy tính sẽ thay đổi tùy theo độ sâu bit.

Bây giờ là lúc để nhớ các số thực được lưu trữ trong bộ nhớ máy tính như thế nào. Nói chung, một số thực bao gồm ba phần chính - dấu, phần định trị và số mũ. Dấu có thể là dấu cộng hoặc dấu trừ, do đó một bit được phân bổ cho nó. Nhưng số bit của phần định trị và số mũ được xác định theo kiểu thực. Vì vậy, đối với loại REAL, độ dài của phần định trị là 23 bit (một bit bằng 1 được ngầm thêm vào phần đầu của phần định trị và kết quả là 24) và số mũ là 8 bit. Tổng cộng là 32 bit, hoặc 4 byte. Và đối với loại CHÍNH XÁC NHÂN ĐÔI, độ dài của phần định trị sẽ là 52 bit và số mũ sẽ là 11 bit, tổng cộng là 64 bit hoặc 8 byte. PostgreSQL không hỗ trợ độ chính xác cao hơn cho số dấu phẩy động.

Hãy gói số thập phân 0.1 của chúng ta thành cả hai loại CHÍNH XÁC THỰC và ĐỘ CHÍNH XÁC NHÂN ĐÔI. Vì dấu và giá trị của số mũ là như nhau nên chúng ta sẽ tập trung vào phần định trị (Tôi cố tình bỏ qua các tính năng không rõ ràng là lưu trữ giá trị của số mũ và giá trị thực bằng 754, vì chúng làm phức tạp sự hiểu biết và làm xao lãng bản chất của vấn đề, nếu quan tâm, hãy xem tiêu chuẩn IEEE 1). Chúng ta sẽ nhận được gì? Ở dòng trên cùng, tôi sẽ cung cấp “mantissa” cho loại REAL (có tính đến việc làm tròn bit cuối cùng lên 0.099999 thành số có thể biểu thị gần nhất, nếu không nó sẽ là XNUMX...), và ở dòng dưới cùng - dành cho loại CHÍNH XÁC NHÂN ĐÔI:

0.000110011001100110011001101
0.00011001100110011001100110011001100110011001100110011001

Rõ ràng đây là hai con số hoàn toàn khác nhau! Do đó, khi so sánh, số đầu tiên sẽ được đệm bằng các số 0.1 và do đó sẽ lớn hơn số thứ hai (có tính đến việc làm tròn - số được đánh dấu in đậm). Điều này giải thích sự mơ hồ từ các ví dụ của chúng tôi. Trong ví dụ thứ hai, số XNUMX được chỉ định rõ ràng được chuyển sang loại CHÍNH XÁC NHÂN ĐÔI, sau đó được so sánh với một số loại THỰC. Cả hai đều được rút gọn thành cùng một loại và chúng ta có chính xác những gì chúng ta thấy ở trên. Hãy sửa đổi truy vấn để mọi thứ rơi vào đúng vị trí:

********* ЗАПРОС *********
SELECT 0.1::REAL > 0.1::DOUBLE PRECISION;
**************************
?column?
----------
t
(1 строка)

Và thực sự, bằng cách thực hiện giảm gấp đôi số 0.1 thành CHÍNH XÁC THỰC SỰ và NHÂN ĐÔI, chúng ta sẽ có được câu trả lời cho câu đố:

********* ЗАПРОС *********
SELECT 0.1::REAL::DOUBLE PRECISION;
**************************

      float8       
-------------------
0.100000001490116
(1 строка)

Điều này cũng giải thích ví dụ thứ ba ở trên. Con số 123 thật đơn giản không thể ghép phần định trị thành 24 bit (23 rõ ràng + 1 ngụ ý). Số nguyên tối đa có thể vừa với 24 bit là 224-1 = 16. Do đó, số 777 của chúng tôi được làm tròn đến số có thể biểu thị gần nhất là 215. Bằng cách thay đổi loại thành ĐỘ CHÍNH XÁC NHÂN ĐÔI, chúng tôi không còn thấy tình huống này nữa:

********* ЗАПРОС *********
SELECT 123456789::DOUBLE PRECISION::INT;
**************************
   int4   
-----------
123456789
(1 строка)

Đó là tất cả. Hóa ra không có phép lạ nào cả. Nhưng mọi thứ được mô tả là lý do chính đáng để suy nghĩ xem bạn thực sự cần loại THỰC SỰ đến mức nào. Có lẽ ưu điểm lớn nhất của việc sử dụng nó là tốc độ tính toán với độ chính xác được biết đến là thấp. Nhưng liệu đây có phải là một kịch bản phổ biến có thể biện minh cho việc sử dụng thường xuyên loại hình này? Đừng nghĩ.

Nguồn: www.habr.com

Thêm một lời nhận xét