Tõeliste tüüpide ebareaalsed omadused või olge REALiga ettevaatlik

Pärast avaldamist artiklid PostgreSQL-i tippimise funktsioonide kohta oli kõige esimene kommentaar reaalarvudega töötamise raskuste kohta. Otsustasin heita kiire pilgu mulle saadaolevate SQL-päringute koodidele, et näha, kui sageli nad kasutavad REAL-tüüpi. Selgub, et seda kasutatakse üsna sageli ja arendajad ei mõista alati selle taga peituvaid ohte. Ja seda hoolimata asjaolust, et Internetis ja Habré lehel on üsna palju häid artikleid reaalarvude arvutimällu salvestamise ja nendega töötamise funktsioonide kohta. Seetõttu püüan selles artiklis selliseid funktsioone PostgreSQL-ile rakendada ja proovin heita kiire pilgu nendega seotud probleemidele, et SQL päringu arendajatel oleks neid lihtsam vältida.

PostgreSQL-i dokumentatsioon ütleb lühidalt: "Selliste vigade haldamine ja nende levik arvutamisel on terve matemaatika ja arvutiteaduse haru teema ning seda siin ei käsitleta" (viidates samas lugejat targalt IEEE 754 standardile). Milliseid vigu siin silmas peetakse? Arutame neid järjekorras ja peagi selgub, miks ma uuesti pastaka kätte võtsin.

Võtame näiteks lihtsa taotluse:

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

Selle tulemusena me midagi erilist ei näe – saame oodatud 0.1. Aga nüüd võrdleme seda 0.1-ga:

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

Pole võrdne! Millised imed! Aga edasi, rohkem. Keegi ütleb: ma tean, et REAL käitub murdudega halvasti, nii et sisestan sinna täisarvud ja nendega on kõik kindlasti korras. Ok, anname numbri 123 456 789 REALile:

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

Ja tuli veel 3! See on kõik, andmebaas on lõpuks unustanud, kuidas lugeda! Või saame millestki valesti aru? Selgitame välja.

Kõigepealt meenutagem materjali. Nagu teate, saab iga kümnendarvu laiendada kümnendarvuks. Seega on arv 123.456 võrdne 1*102 + 2*101 + 3*100 + 4*10-1 + 5*10-2 + ​​6*10-3. Kuid arvuti töötab numbritega binaarses vormis, seetõttu tuleb need esitada kahe astme laiendusena. Seetõttu on kahendarvuna arv 5.625 esitatud kui 101.101 ja see võrdub 1*22 + 0*21 + 1*20 + 1*2-1 + 0*2-2 + 1*2-3. Ja kui kahe positiivsed astmed annavad alati täisarvud koma (1, 2, 4, 8, 16 jne), siis negatiivsetega on kõik keerulisem (0.5, 0.25, 0.125, 0,0625 jne). Probleem on selles Iga kümnendkohta ei saa esitada lõpliku kahendmurruna. Seega ilmub meie kurikuulus 0.1 kahendmurru kujul perioodilise väärtusena 0.0(0011). Järelikult varieerub selle arvu lõplik väärtus arvutimälus sõltuvalt biti sügavusest.

Nüüd on aeg meeles pidada, kuidas reaalarvud arvuti mällu salvestatakse. Üldiselt koosneb reaalarv kolmest põhiosast – märgist, mantissist ja astendajast. Märk võib olla kas pluss või miinus, seega eraldatakse selle jaoks üks bitt. Kuid mantissi ja eksponendi bittide arvu määrab tegelik tüüp. Niisiis, REAL tüübi puhul on mantissi pikkus 23 bitti (mantissi algusesse lisatakse kaudselt üks bitt, mis võrdub 1, ja tulemus on 24) ja eksponent on 8 bitti. Kokku on 32 bitti ehk 4 baiti. Ja DOUBLE PRECISION tüübi puhul on mantissi pikkus 52 bitti ja eksponent 11 bitti, kokku 64 bitti ehk 8 baiti. PostgreSQL ei toeta ujukomaarvude suuremat täpsust.

Pakkime oma kümnendarvu 0.1 nii REAL kui ka DOUBLE PRECISION tüüpidesse. Kuna eksponendi märk ja väärtus on samad, keskendume mantissale (jätan teadlikult välja eksponendi väärtuste ja nulli tegelike väärtuste salvestamise mitteilmsemad tunnused, kuna need raskendavad mõistmist ja juhivad tähelepanu olemusest kõrvale Kui olete huvitatud, vaadake IEEE 754 standardit). Mida me saame? Ülemisel real annan PÄRIS tüübi “mantissa” (võttes arvesse viimase biti ümardamist 1 võrra lähima esindatava arvuni, muidu on see 0.099999...) ja alumisel real - jaoks. DOUBLE PRECISION tüüp:

0.000110011001100110011001101
0.00011001100110011001100110011001100110011001100110011001

Ilmselgelt on need kaks täiesti erinevat numbrit! Seetõttu on võrdlemisel esimene number polsterdatud nullidega ja seepärast on see suurem kui teine ​​(arvestades ümardamist - see, mis on märgitud paksus kirjas). See selgitab meie näidete ebaselgust. Teises näites kantakse selgesõnaliselt määratud arv 0.1 tüüpi DOUBLE PRECISION ja võrreldakse seejärel arvuga REAL tüüpi. Mõlemad on taandatud samale tüübile ja meil on täpselt see, mida näeme ülal. Muudame päringut nii, et kõik paika loksuks:

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

Ja tõepoolest, tehes arvu 0.1 kahekordse taandamise REAALSEKS ja TOPELTTÄPSUSEKS, saame vastuse mõistatusele:

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

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

See selgitab ka ülaltoodud kolmandat näidet. Number 123 456 789 on lihtne mantissi on võimatu 24 bitti mahutada (23 selgesõnalist + 1 kaudset). Maksimaalne täisarv, mis mahub 24 bitti, on 224-1 = 16 777 215. Seetõttu ümardatakse meie arv 123 456 789 lähima esindatava 123 456 792-ni. Kui muudate tüübiks TOOPEPRETSIOON, ei näe me enam seda stsenaariumi:

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

See on kõik. Selgub, et imesid pole olemas. Kuid kõik kirjeldatu annab hea põhjuse mõelda, kui palju te tegelikult vajate PÄRIS tüüpi. Võib-olla on selle kasutamise suurim eelis teadaoleva täpsuse vähenemisega arvutuste kiirus. Kuid kas see oleks universaalne stsenaarium, mis õigustaks seda tüüpi sagedast kasutamist? Ära mõtle.

Allikas: www.habr.com

Lisa kommentaar