Ciri-ciri Tidak Nyata dari Tipe Nyata, atau Hati-hati dengan NYATA

Setelah publikasi Artikel tentang fitur pengetikan di PostgreSQL, komentar pertama adalah tentang kesulitan bekerja dengan bilangan real. Saya memutuskan untuk melihat sekilas kode kueri SQL yang tersedia bagi saya untuk melihat seberapa sering mereka menggunakan tipe REAL. Ternyata cukup sering digunakan, dan developer tidak selalu memahami bahaya di baliknya. Dan ini terlepas dari kenyataan bahwa ada cukup banyak artikel bagus di Internet dan di Habré tentang fitur menyimpan bilangan real dalam memori komputer dan tentang cara menggunakannya. Oleh karena itu, dalam artikel ini saya akan mencoba menerapkan fitur-fitur tersebut ke PostgreSQL, dan mencoba melihat sekilas masalah yang terkait dengannya, sehingga akan lebih mudah bagi pengembang kueri SQL untuk menghindarinya.

Dokumentasi PostgreSQL menyatakan dengan singkat: “Pengelolaan kesalahan tersebut dan penyebarannya selama komputasi adalah subjek dari seluruh cabang matematika dan ilmu komputer, dan tidak tercakup di sini” (sementara dengan bijak merujuk pembaca ke standar IEEE 754). Kesalahan apa yang dimaksud di sini? Mari kita bahas secara berurutan, dan akan segera menjadi jelas mengapa saya menggunakan pena lagi.

Mari kita ambil contoh permintaan sederhana:

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

Akibatnya, kita tidak akan melihat sesuatu yang istimewa – kita akan mendapatkan 0.1 yang diharapkan. Tapi sekarang mari kita bandingkan dengan 0.1:

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

Tidak sama! Sungguh keajaiban! Namun lebih jauh lagi. Seseorang akan berkata, saya tahu REAL berperilaku buruk dengan pecahan, jadi saya akan memasukkan bilangan bulat di sana, dan semuanya pasti akan baik-baik saja. Oke, mari kita masukkan angka 123 ke REAL:

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

Dan ternyata ada 3 lagi! Itu saja, database akhirnya lupa cara menghitung! Atau apakah kita salah memahami sesuatu? Mari kita cari tahu.

Pertama, mari kita ingat materinya. Seperti yang Anda ketahui, bilangan desimal apa pun dapat dipangkatkan sepuluh. Jadi, angka 123.456 akan sama dengan 1*102 + 2*101 + 3*100 + 4*10-1 + 5*10-2 + ​​​​6*10-3. Namun komputer beroperasi dengan bilangan dalam bentuk biner, oleh karena itu bilangan tersebut harus direpresentasikan dalam bentuk perluasan pangkat dua. Oleh karena itu, angka 5.625 dalam biner direpresentasikan sebagai 101.101 dan akan sama dengan 1*22 + 0*21 + 1*20 + 1*2-1 + 0*2-2 + 1*2-3. Dan jika pangkat positif dari dua selalu menghasilkan bilangan desimal utuh (1, 2, 4, 8, 16, dst.), maka dengan pangkat negatif semuanya menjadi lebih rumit (0.5, 0.25, 0.125, 0,0625, dst). Masalahnya adalah Tidak setiap desimal dapat direpresentasikan sebagai pecahan biner berhingga. Jadi, 0.1 yang terkenal dalam bentuk pecahan biner muncul sebagai nilai periodik 0.0(0011). Akibatnya, nilai akhir angka ini di memori komputer akan bervariasi tergantung pada kedalaman bit.

Sekaranglah waktunya untuk mengingat bagaimana bilangan real disimpan dalam memori komputer. Secara umum, bilangan real terdiri dari tiga bagian utama - tanda, mantissa, dan eksponen. Tandanya bisa plus atau minus, jadi satu bit dialokasikan untuk itu. Namun jumlah bit mantissa dan eksponen ditentukan oleh tipe sebenarnya. Jadi, untuk tipe REAL, panjang mantissa adalah 23 bit (satu bit sama dengan 1 ditambahkan secara implisit ke awal mantissa, dan hasilnya adalah 24 bit), dan eksponennya adalah 8 bit. Totalnya adalah 32 bit, atau 4 byte. Dan untuk tipe DOUBLE PRECISION, panjang mantissa adalah 52 bit, dan eksponennya adalah 11 bit, sehingga totalnya adalah 64 bit atau 8 byte. PostgreSQL tidak mendukung presisi yang lebih tinggi untuk angka floating point.

Mari kita kemas angka desimal 0.1 ke dalam tipe REAL dan DOUBLE PRECISION. Karena tanda dan nilai eksponennya sama, kita akan fokus pada mantissa (saya sengaja menghilangkan fitur yang tidak jelas dalam menyimpan nilai eksponen dan nilai real nol, karena mempersulit pemahaman dan mengalihkan perhatian dari esensi. masalahnya, jika tertarik, lihat standar IEEE 754). Apa yang akan kita dapatkan? Di baris paling atas saya akan memberikan "mantissa" untuk tipe REAL (dengan mempertimbangkan pembulatan bit terakhir sebesar 1 ke angka terdekat yang dapat diwakili, jika tidak maka akan menjadi 0.099999...), dan di baris paling bawah - untuk tipe PRESISI GANDA:

0.000110011001100110011001101
0.00011001100110011001100110011001100110011001100110011001

Jelas sekali ini adalah dua angka yang sangat berbeda! Oleh karena itu, ketika membandingkan, angka pertama akan diisi dengan nol dan, oleh karena itu, akan lebih besar dari angka kedua (dengan mempertimbangkan pembulatan - yang ditandai dengan huruf tebal). Ini menjelaskan ambiguitas dari contoh kita. Dalam contoh kedua, angka 0.1 yang ditentukan secara eksplisit dimasukkan ke tipe DOUBLE PRECISION, dan kemudian dibandingkan dengan angka tipe REAL. Keduanya direduksi menjadi tipe yang sama, dan kita mendapatkan apa yang kita lihat di atas. Mari kita ubah kuerinya agar semuanya sesuai:

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

Dan memang, dengan melakukan pengurangan ganda angka 0.1 menjadi NYATA dan PRESISI GANDA, kita mendapatkan jawaban dari teka-teki tersebut:

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

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

Ini juga menjelaskan contoh ketiga di atas. Angka 123 itu sederhana tidak mungkin memasukkan mantissa ke dalam 24 bit (23 eksplisit + 1 tersirat). Bilangan bulat maksimum yang dapat ditampung dalam 24 bit adalah 224-1 = 16. Oleh karena itu, angka kita 777 dibulatkan ke representasi terdekat 215. Dengan mengubah tipe menjadi DOUBLE PRECISION, kita tidak lagi melihat skenario ini:

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

Itu saja. Ternyata tidak ada keajaiban. Tapi semua yang dijelaskan adalah alasan bagus untuk memikirkan betapa Anda benar-benar membutuhkan tipe REAL. Mungkin keuntungan terbesar dari penggunaannya adalah kecepatan penghitungan dengan hilangnya akurasi yang diketahui. Namun apakah ini merupakan skenario universal yang membenarkan seringnya penggunaan jenis ini? Jangan berpikir.

Sumber: www.habr.com

Tambah komentar