Nerealūs tikrų tipų bruožai arba būkite atsargūs su REAL

Po paskelbimo Straipsnis apie spausdinimo PostgreSQL ypatybes, pats pirmasis komentaras buvo apie sunkumus dirbant su realiais skaičiais. Nusprendžiau greitai pažvelgti į man prieinamų SQL užklausų kodą, kad pamatyčiau, kaip dažnai jie naudoja REAL tipą. Pasirodo, jis naudojamas gana dažnai, o kūrėjai ne visada supranta už jo slypinčius pavojus. Ir tai nepaisant to, kad internete ir Habré yra gana daug gerų straipsnių apie realių skaičių saugojimo kompiuterio atmintyje ypatybes ir apie darbą su jais. Todėl šiame straipsnyje pabandysiu tokias funkcijas pritaikyti PostgreSQL ir pasistengsiu greitai apžvelgti su jomis susijusias bėdas, kad SQL užklausų kūrėjams būtų lengviau jų išvengti.

Документация PostgreSQL содержит лаконичную фразу: «Управление подобными ошибками и их распространение в процессе вычислений является предметом изучения целого раздела математики и компьютерной науки, и здесь не рассматривается» (при этом благоразумно отсылая читателя к стандарту IEEE 754). Что за ошибки здесь имеются в виду? Давайте обсудим их по-порядку, и скоро станет понятно, почему я снова взялся за перо.

Paimkime, pavyzdžiui, paprastą prašymą:

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

В результате не увидим ничего особенного – получим ожидаемое 0.1. Но теперь сравним его с 0.1:

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

Не равны! Что за чудеса! Но дальше-больше. Кто-то скажет, мол, я знаю, что REAL плохо ведет себя с дробями, ну так я буду туда заносить целые числа, с ними-то точно все будет хорошо. Ок, давайте приведем число 123 456 789 к типу REAL:

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

А оно получилось больше на 3! Все, база окончательно разучилась считать! Или мы чего-то недопонимаем? Давайте разбираться.

Pirmiausia prisiminkime medžiagą. Kaip žinote, bet kurį dešimtainį skaičių galima išplėsti iki dešimties. Taigi, skaičius 123.456 bus lygus 1*102 + 2*101 + 3*100 + 4*10-1 + 5*10-2 + ​​​​6*10-3. Tačiau kompiuteris veikia su skaičiais dvejetaine forma, todėl jie turi būti pavaizduoti plėtimosi forma dviejų laipsniais. Todėl skaičius 5.625 dvejetainiu formatu yra vaizduojamas kaip 101.101 ir bus lygus 1*22 + 0*21 + 1*20 + 1*2-1 + 0*2-2 + 1*2-3. Ir jei teigiami dviejų laipsniai visada suteikia sveikus dešimtainius skaičius (1, 2, 4, 8, 16 ir tt), tai su neigiamais viskas yra sudėtingiau (0.5, 0.25, 0.125, 0,0625 ir kt.). Problema ta не всякую десятичную дробь можно представить в виде конечной двоичной дроби. Так, наше пресловутое 0.1 в виде двоичной дроби предстает как периодическое значение 0.0(0011). Следовательно, итоговое значение этого числа в машинной памяти будет меняться в зависимости от разрядности.

Dabar pats laikas prisiminti, kaip tikrieji skaičiai saugomi kompiuterio atmintyje. Paprastai tariant, realusis skaičius susideda iš trijų pagrindinių dalių – ženklo, mantisos ir eksponento. Ženklas gali būti pliusas arba minusas, todėl jam skiriamas vienas bitas. Tačiau mantisos ir eksponento bitų skaičius nustatomas pagal tikrąjį tipą. Taigi REAL tipo mantisos ilgis yra 23 bitai (vienas bitas, lygus 1, netiesiogiai pridedamas prie mantisos pradžios, o rezultatas yra 24), o eksponentas yra 8 bitai. Iš viso yra 32 bitai arba 4 baitai. O DOUBLE PRECISION tipo mantisos ilgis bus 52 bitai, o eksponentas – 11 bitų, iš viso 64 bitai arba 8 baitai. PostgreSQL nepalaiko didesnio tikslumo slankiojo kablelio skaičiams.

Sudėkime dešimtainį skaičių 0.1 į REAL ir DOUBLE PRECISION tipus. Kadangi eksponento ženklas ir reikšmė yra vienodi, mes sutelksime dėmesį į mantisą (aš sąmoningai praleidžiu neakivaizdžius eksponento verčių ir nulinių realių reikšmių saugojimo ypatumus, nes jie apsunkina supratimą ir atitraukia dėmesį nuo esmės Jei domina, žr. IEEE 754 standartą). Ką mes gausime? Viršutinėje eilutėje pateiksiu „mantisą“, skirtą REAL tipui (atsižvelgiant į paskutinio bito apvalinimą 1 iki artimiausio reprezentuojamo skaičiaus, kitaip jis bus 0.099999...), o apatinėje eilutėje - už. DOUBLE PRECISION tipas:

0.000110011001100110011001101
0.00011001100110011001100110011001100110011001100110011001

Akivaizdu, kad tai yra du visiškai skirtingi skaičiai! Todėl lyginant pirmasis skaičius bus parašytas nuliais, todėl bus didesnis nei antrasis (atsižvelgiant į apvalinimą – pažymėtas paryškintu šriftu). Tai paaiškina mūsų pavyzdžių dviprasmiškumą. Antrajame pavyzdyje aiškiai nurodytas skaičius 0.1 perduodamas DVIGUBAS TIKSLUMO tipui, o tada lyginamas su REAL tipo skaičiumi. Abi yra sumažintos iki to paties tipo, ir mes turime būtent tai, ką matome aukščiau. Pakeiskime užklausą, kad viskas atsidurtų savo vietose:

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

И действительно, выполнив двойное приведение числа 0.1 к REAL и DOUBLE PRECISION получаем ответ на загадку:

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

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

Этим же объясняется и третий пример из указанных выше. Число 123 456 789 просто neįmanoma sutalpinti mantisos į 24 bitus (23 явных + 1 подразумеваемый). Максимальное целое число, которое можно разместить в 24 бита, будет 224-1 = 16 777 215. Поэтому наше число 123 456 789 округляется до ближайшего представимого 123 456 792. Сменив тип на DOUBLE PRECISION, мы уже не увидим такого сценария:

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

Вот и все. Оказывается, никаких чудес. Но все описанное – хороший повод призадуматься на предмет того, насколько вам действительно нужен тип REAL. Пожалуй, самый большой плюс его использования – быстрота вычислений с заведомо имеющейся потерей точности. Но будет ли это универсальным сценарием, оправдывающим столь частое использование этого типа? Не думаю.

Šaltinis: www.habr.com

Добавить комментарий