Mga Hindi Tunay na Tampok ng Mga Tunay na Uri, o Mag-ingat sa TOTOO

Pagkatapos ng publikasyon Artikulo tungkol sa mga tampok ng pag-type sa PostgreSQL, ang pinakaunang komento ay tungkol sa mga kahirapan sa pagtatrabaho sa mga totoong numero. Napagpasyahan kong tingnan ang code ng mga query sa SQL na magagamit ko upang makita kung gaano kadalas nila ginagamit ang TUNAY na uri. Lumalabas na madalas itong ginagamit, at hindi palaging nauunawaan ng mga developer ang mga panganib sa likod nito. At ito sa kabila ng katotohanan na mayroong maraming magagandang artikulo sa Internet at sa HabrΓ© tungkol sa mga tampok ng pag-iimbak ng mga tunay na numero sa memorya ng computer at tungkol sa pagtatrabaho sa kanila. Samakatuwid, sa artikulong ito ay susubukan kong ilapat ang mga naturang feature sa PostgreSQL, at susubukan kong tingnan ang mga kaguluhang nauugnay sa kanila, upang maging mas madali para sa mga developer ng SQL query na maiwasan ang mga ito.

Ang dokumentasyon ng PostgreSQL ay malinaw na nagsasaad: "Ang pamamahala ng mga naturang error at ang kanilang pagpapalaganap sa panahon ng pagkalkula ay paksa ng isang buong sangay ng matematika at computer science, at hindi saklaw dito" (habang matalinong tinutukoy ang mambabasa sa pamantayang IEEE 754). Anong uri ng mga pagkakamali ang ibig sabihin dito? Pag-usapan natin ang mga ito sa pagkakasunud-sunod, at magiging malinaw kung bakit muli kong kinuha ang panulat.

Kunin natin halimbawa ang isang simpleng kahilingan:

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

Bilang resulta, wala kaming makikitang espesyal - makukuha namin ang inaasahang 0.1. Ngunit ngayon ihambing natin ito sa 0.1:

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

Hindi pantay! Anong mga himala! Ngunit higit pa, higit pa. May magsasabi, I know that REAL behaves badly with fractions, so I'll enter whole numbers there, and everything will definitely be fine with them. Ok, i-cast natin ang numerong 123 sa REAL:

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

At naging 3 pa! Iyon lang, sa wakas ay nakalimutan na ng database kung paano magbilang! O may hindi tayo pagkakaintindihan? Alamin natin ito.

Una, tandaan natin ang materyal. Tulad ng alam mo, ang anumang decimal na numero ay maaaring palawakin sa kapangyarihan ng sampu. Kaya, ang bilang na 123.456 ay magiging katumbas ng 1*102 + 2*101 + 3*100 + 4*10-1 + 5*10-2 + ​​​​6*10-3. Ngunit ang computer ay nagpapatakbo ng mga numero sa binary form, samakatuwid sila ay dapat na kinakatawan sa anyo ng pagpapalawak sa kapangyarihan ng dalawa. Samakatuwid, ang bilang na 5.625 sa binary ay kinakatawan bilang 101.101 at magiging katumbas ng 1*22 + 0*21 + 1*20 + 1*2-1 + 0*2-2 + 1*2-3. At kung ang mga positibong kapangyarihan ng dalawa ay palaging nagbibigay ng buong mga numero ng decimal (1, 2, 4, 8, 16, atbp.), kung gayon sa mga negatibo ang lahat ay mas kumplikado (0.5, 0.25, 0.125, 0,0625, atbp.). Ang problema ay iyon Hindi lahat ng decimal ay maaaring katawanin bilang isang finite binary fraction. Kaya, ang aming kilalang-kilala na 0.1 sa anyo ng isang binary fraction ay lilitaw bilang periodic na halaga 0.0(0011). Dahil dito, ang huling halaga ng numerong ito sa memorya ng computer ay mag-iiba depende sa lalim ng bit.

Ngayon ang oras upang tandaan kung paano nakaimbak ang mga tunay na numero sa memorya ng computer. Sa pangkalahatan, ang isang tunay na numero ay binubuo ng tatlong pangunahing bahagi - sign, mantissa at exponent. Ang sign ay maaaring maging plus o minus, kaya isang bit ang inilalaan para dito. Ngunit ang bilang ng mga bit ng mantissa at exponent ay tinutukoy ng tunay na uri. Kaya, para sa TUNAY na uri, ang haba ng mantissa ay 23 bits (isang bit na katumbas ng 1 ay implicitly na idinagdag sa simula ng mantissa, at ang resulta ay 24), at ang exponent ay 8 bits. Ang kabuuan ay 32 bits, o 4 bytes. At para sa uri ng DOUBLE PRECISION, ang haba ng mantissa ay magiging 52 bits, at ang exponent ay magiging 11 bits, para sa kabuuang 64 bits, o 8 bytes. Hindi sinusuportahan ng PostgreSQL ang mas mataas na katumpakan para sa mga numero ng floating point.

I-pack natin ang ating decimal na numero na 0.1 sa parehong mga uri ng REAL at DOUBLE PRECISION. Dahil ang tanda at halaga ng exponent ay pareho, kami ay tumutuon sa mantissa (sinadya kong tinanggal ang mga hindi halatang tampok ng pag-iimbak ng mga halaga ng exponent at zero na tunay na mga halaga, dahil ginagawang kumplikado ang pag-unawa at pagkagambala mula sa kakanyahan. ng problema, kung interesado, tingnan ang IEEE 754 standard). Ano ang makukuha natin? Sa tuktok na linya ay ibibigay ko ang "mantissa" para sa TUNAY na uri (isinasaalang-alang ang pag-ikot ng huling bit ng 1 sa pinakamalapit na representable na numero, kung hindi man ito ay magiging 0.099999...), at sa ilalim na linya - para sa ang uri ng DOUBLE PRECISION:

0.000110011001100110011001101
0.00011001100110011001100110011001100110011001100110011001

Malinaw na ang mga ito ay dalawang ganap na magkaibang mga numero! Samakatuwid, kapag naghahambing, ang unang numero ay lagyan ng mga zero at, samakatuwid, ay magiging mas malaki kaysa sa pangalawa (isinasaalang-alang ang pag-ikot - ang minarkahan ng bold). Ipinapaliwanag nito ang kalabuan mula sa aming mga halimbawa. Sa pangalawang halimbawa, ang tahasang tinukoy na numero 0.1 ay inihagis sa DOUBLE PRECISION na uri, at pagkatapos ay inihambing sa isang bilang ng REAL na uri. Parehong binawasan sa parehong uri, at mayroon kaming eksakto kung ano ang nakikita namin sa itaas. Baguhin natin ang query para maayos ang lahat:

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

At sa katunayan, sa pamamagitan ng pagsasagawa ng dobleng pagbawas ng numero 0.1 sa TOTOO at DOUBLE PECISION, makukuha natin ang sagot sa bugtong:

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

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

Ipinapaliwanag din nito ang ikatlong halimbawa sa itaas. Ang bilang na 123 ay simple imposibleng magkasya ang mantissa sa 24 bits (23 tahasang + 1 ang ipinahiwatig). Ang maximum na integer na maaaring magkasya sa 24 bits ay 224-1 = 16. Samakatuwid, ang aming numero na 777 ay bilugan sa pinakamalapit na representable na 215. Sa pamamagitan ng pagpapalit ng uri sa DOUBLE PRECISION, hindi na namin nakikita ang sitwasyong ito:

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

Iyon lang. Ito ay lumiliko na walang mga himala. Ngunit ang lahat ng inilarawan ay isang magandang dahilan upang isipin kung gaano mo talaga kailangan ang TUNAY na uri. Marahil ang pinakamalaking bentahe ng paggamit nito ay ang bilis ng mga kalkulasyon na may kilalang pagkawala ng katumpakan. Ngunit ito ba ay isang unibersal na senaryo na magbibigay-katwiran sa madalas na paggamit ng ganitong uri? wag mong isipin.

Pinagmulan: www.habr.com

Magdagdag ng komento