Ciri Tidak Sebenar bagi Jenis Sebenar, atau Berhati-hati dengan SEBENAR

Selepas diterbitkan Perkara mengenai ciri-ciri menaip dalam PostgreSQL, ulasan pertama adalah mengenai kesukaran bekerja dengan nombor nyata. Saya memutuskan untuk melihat dengan pantas kod pertanyaan SQL yang tersedia untuk saya untuk melihat kekerapan mereka menggunakan jenis REAL. Ternyata ia digunakan agak kerap, dan pemaju tidak selalu memahami bahaya di belakangnya. Dan ini walaupun terdapat banyak artikel bagus di Internet dan di HabrΓ© tentang ciri-ciri menyimpan nombor nyata dalam memori komputer dan tentang bekerja dengan mereka. Oleh itu, dalam artikel ini saya akan cuba menggunakan ciri tersebut pada PostgreSQL, dan akan cuba melihat dengan pantas masalah yang berkaitan dengannya, supaya lebih mudah bagi pembangun pertanyaan SQL untuk mengelakkannya.

Dokumentasi PostgreSQL menyatakan dengan ringkas: "Pengurusan ralat tersebut dan penyebarannya semasa pengiraan adalah subjek keseluruhan cabang matematik dan sains komputer, dan tidak dibincangkan di sini" (sambil bijak merujuk pembaca kepada standard IEEE 754). Apakah jenis ralat yang dimaksudkan di sini? Mari kita bincangkannya dengan teratur, dan tidak lama lagi akan menjadi jelas mengapa saya mengambil pen itu lagi.

Mari kita ambil contoh permintaan mudah:

********* Π—ΠΠŸΠ ΠžΠ‘ *********
SELECT 0.1::REAL;
**************************
float4
--------
    0.1
(1 строка)

Akibatnya, kami tidak akan melihat sesuatu yang istimewa - kami akan mendapat 0.1 yang dijangkakan. Tetapi sekarang mari kita bandingkan dengan 0.1:

********* Π—ΠΠŸΠ ΠžΠ‘ *********
SELECT 0.1::REAL = 0.1;
**************************
?column?
----------
f
(1 строка)

Tidak sama! Apa keajaiban! Tetapi lebih jauh lagi. Seseorang akan berkata, saya tahu bahawa REAL berkelakuan buruk dengan pecahan, jadi saya akan memasukkan nombor bulat di sana, dan semuanya pasti akan baik dengan mereka. Ok, mari hantar nombor 123 ke REAL:

********* Π—ΠΠŸΠ ΠžΠ‘ *********
SELECT 123456789::REAL::INT;
**************************
   int4   
-----------
123456792
(1 строка)

Dan ternyata 3 lagi! Itu sahaja, pangkalan data akhirnya lupa cara mengira! Atau adakah kita salah faham sesuatu? Mari kita fikirkan.

Pertama, mari kita ingat bahan. Seperti yang anda ketahui, sebarang nombor perpuluhan boleh dikembangkan menjadi kuasa sepuluh. Jadi, nombor 123.456 akan bersamaan dengan 1*102 + 2*101 + 3*100 + 4*10-1 + 5*10-2 + ​​​​6*10-3. Tetapi komputer beroperasi dengan nombor dalam bentuk binari, oleh itu mereka perlu diwakili dalam bentuk pengembangan dalam kuasa dua. Oleh itu, nombor 5.625 dalam binari diwakili sebagai 101.101 dan akan bersamaan dengan 1*22 + 0*21 + 1*20 + 1*2-1 + 0*2-2 + 1*2-3. Dan jika kuasa positif dua sentiasa memberikan nombor perpuluhan penuh (1, 2, 4, 8, 16, dll.), maka dengan yang negatif semuanya lebih rumit (0.5, 0.25, 0.125, 0,0625, dll.). Masalahnya ialah Tidak setiap perpuluhan boleh diwakili sebagai pecahan binari terhingga. Oleh itu, 0.1 kami yang terkenal dalam bentuk pecahan binari muncul sebagai nilai berkala 0.0(0011). Akibatnya, nilai akhir nombor ini dalam memori komputer akan berbeza-beza bergantung pada kedalaman bit.

Sekarang adalah masa untuk mengingati bagaimana nombor nyata disimpan dalam memori komputer. Secara umumnya, nombor nyata terdiri daripada tiga bahagian utama - tanda, mantissa dan eksponen. Tanda boleh sama ada tambah atau tolak, jadi satu bit diperuntukkan untuknya. Tetapi bilangan bit mantissa dan eksponen ditentukan oleh jenis sebenar. Jadi, untuk jenis REAL, panjang mantissa ialah 23 bit (satu bit sama dengan 1 secara tersirat ditambahkan pada permulaan mantissa, dan hasilnya ialah 24), dan eksponen ialah 8 bit. Jumlahnya ialah 32 bit, atau 4 bait. Dan untuk jenis DOUBLE PECISION, panjang mantissa ialah 52 bit, dan eksponen ialah 11 bit, dengan jumlah keseluruhan 64 bit, atau 8 bait. PostgreSQL tidak menyokong ketepatan yang lebih tinggi untuk nombor titik terapung.

Mari bungkus nombor perpuluhan kita 0.1 ke dalam kedua-dua jenis REAL dan DOUBLE PECISION. Oleh kerana tanda dan nilai eksponen adalah sama, kami akan memberi tumpuan kepada mantissa (saya sengaja menghilangkan ciri yang tidak jelas untuk menyimpan nilai eksponen dan sifar nilai sebenar, kerana ia merumitkan pemahaman dan mengalih perhatian daripada intipati masalah, jika berminat, lihat standard IEEE 754). Apa yang kita akan dapat? Di baris atas saya akan memberikan "mantissa" untuk jenis REAL (dengan mengambil kira pembundaran bit terakhir dengan 1 kepada nombor yang boleh diwakili terdekat, jika tidak, ia akan menjadi 0.099999...), dan di baris bawah - untuk jenis DOUBLE PRESISION:

0.000110011001100110011001101
0.00011001100110011001100110011001100110011001100110011001

Jelas sekali ini adalah dua nombor yang sama sekali berbeza! Oleh itu, apabila membandingkan, nombor pertama akan dipadatkan dengan sifar dan, oleh itu, akan lebih besar daripada yang kedua (dengan mengambil kira pembundaran - yang ditandakan dengan huruf tebal). Ini menjelaskan kekaburan daripada contoh kami. Dalam contoh kedua, nombor 0.1 yang dinyatakan secara eksplisit dibuang ke jenis DOUBLE PECISION, dan kemudian dibandingkan dengan beberapa jenis REAL. Kedua-duanya dikurangkan kepada jenis yang sama, dan kami mempunyai apa yang kami lihat di atas. Mari kita ubah suai pertanyaan supaya semuanya sesuai:

********* Π—ΠΠŸΠ ΠžΠ‘ *********
SELECT 0.1::REAL > 0.1::DOUBLE PRECISION;
**************************
?column?
----------
t
(1 строка)

Dan sesungguhnya, dengan melakukan pengurangan dua kali ganda nombor 0.1 kepada KEJEMPATAN NYATA dan BERGANDA, kita mendapat jawapan kepada teka-teki itu:

********* Π—ΠΠŸΠ ΠžΠ‘ *********
SELECT 0.1::REAL::DOUBLE PRECISION;
**************************

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

Ini juga menerangkan contoh ketiga di atas. Nombor 123 adalah mudah adalah mustahil untuk memuatkan mantissa ke dalam 24 bit (23 eksplisit + 1 tersirat). Integer maksimum yang boleh dimuatkan ke dalam 24 bit ialah 224-1 = 16. Oleh itu, nombor 777 kami dibundarkan kepada 215 yang boleh diwakili terdekat. Dengan menukar jenis kepada DOUBLE PECISION, kami tidak lagi melihat senario ini:

********* Π—ΠΠŸΠ ΠžΠ‘ *********
SELECT 123456789::DOUBLE PRECISION::INT;
**************************
   int4   
-----------
123456789
(1 строка)

Itu sahaja. Ternyata tiada keajaiban. Tetapi semua yang diterangkan adalah alasan yang baik untuk memikirkan betapa anda benar-benar memerlukan jenis SEBENAR. Mungkin kelebihan terbesar penggunaannya ialah kelajuan pengiraan dengan kehilangan ketepatan yang diketahui. Tetapi adakah ini senario sejagat yang akan membenarkan penggunaan jenis ini secara kerap? jangan fikir.

Sumber: www.habr.com

Tambah komen