Egy monitorozó rendszerben a késleltetés, a CPU-terhelés és az időbélyeg gyakran lebegőpontos érték. Ezek azonban nem valós számok korlátlan pontosságú tárolói: véges számú reprezentálható érték közül választanak. Emiatt két különböző időpont azonos tárolt értékké válhat, egy összegzés eredménye függhet a műveleti sorrendtől, és egy optimalizálási kapcsoló megváltoztathatja a numerikus szerződést.
A Promóciók, signedness és túlcsordulás részhez hasonlóan itt is külön kell választanunk a típus, a konverzió és a művelet szabályait. Az alap ISO C17; az IEEE 754 szerinti, kettes alapú működést külön implementációs feltételként kezeljük.
Az ISO C17 három alapvető valós lebegőpontos típusa a
float, a double és a long double.
Értékkészletük egymásba ágyazott: a float értékei a
double, a double értékei a
long double értékkészletének részét képezik. A
long double ettől még nem feltétlenül pontosabb a
double-nél.
A sizeof önmagában nem mondja meg a pontosságot:
tárolási méretet mér C bájtban, amely kitöltést is tartalmazhat. A
numerikus tulajdonságokat a <float.h> írja le.
| Makró | Jelentés |
|---|---|
FLT_RADIX |
A lebegőpontos reprezentáció számrendszerének alapja |
FLT_MANT_DIG, DBL_MANT_DIG,
LDBL_MANT_DIG |
A szignifikand pontossága FLT_RADIX alapú
számjegyekben |
DBL_MIN, DBL_MAX |
A legkisebb pozitív normál, illetve legnagyobb véges
double |
DBL_TRUE_MIN |
A legkisebb pozitív reprezentálható double, a subnormál
értékeket is beleértve |
DBL_EPSILON |
Az
és az azt követő reprezentálható double különbsége |
DBL_DIG |
Biztonságosan megőrizhető decimális pontosság a decimális–bináris–decimális irányban |
DBL_DECIMAL_DIG |
A double–decimális–double
visszaalakításhoz elegendő decimális számjegyszám |
FLT_EVAL_METHOD |
A kifejezések esetleges szélesebb köztes kiértékelésének jelzése |
A DBL_ makróknak megfelelő FLT_ és
LDBL_ változataik is vannak. A subnormál támogatásról a
*_HAS_SUBNORM makrók adnak információt; értékük akár
meghatározhatatlan támogatást is jelezhet.
Konkrét laborprofil, nem ISO-követelmény: Linux,
x86-64 System V ABI, LP64, CHAR_BIT == 8, alapértelmezett
ABI-szerinti long double.
| Típus | Feltételezett formátum | Szignifikand pontossága | Tárolási méret |
|---|---|---|---|
float |
IEEE 754 binary32 | 24 bit | 4 C bájt |
double |
IEEE 754 binary64 | 53 bit | 8 C bájt |
long double |
x87 kiterjesztett formátum | 64 bit | 16 C bájt, ebből 80 bit a numerikus formátum |
Ez nem univerzális „64 bites gépes” táblázat. Más ABI vagy
fordítókapcsoló más long double-t eredményezhet.
A literál típusa is számít: 0.1 típusa
double, 0.1f típusa float,
0.1L típusa long double. A korábban
float-ra kerekített érték későbbi double-re
alakítása nem állítja vissza az elveszett információt.
Kettes alapú formátumban egy nem nulla normál szám alakja:
ahol az előjelbit, a szignifikand pontossága, egész, , pedig a megengedett tartományba eső kitevő.
Azonos kitevő mellett a szomszédos értékek távolsága:
Bizonyításvázlat. Két szomszédos reprezentációban eggyel változik. A szignifikand különbsége ; ezt -vel szorozva kapjuk a távolságot.
Binary64 esetén . Egy körülbelül másodperces Unix-időbélyegre:
ezért
A nagy abszolút időbélyeg tehát már nem képes minden nanoszekundumos
eltérést megkülönböztetni. Ha előbb két időbélyeget
double-re alakítunk, majd kivonjuk őket, az elveszett
részletek nem nyerhetők vissza.
Gyakorlati megoldás: az időpontokat megfelelő egész reprezentációban
tartjuk, tartományellenőrzött kivonással képezzük a különbséget, és csak
az időtartamot alakítjuk másodpercben megadott double-re. A
kisebb nagyságrendhez finomabb abszolút felbontás tartozik.
A subnormál számok a legkisebb normál szám és a nulla közötti tartomány egy részét fedik le, csökkenő relatív pontossággal. Binary64 esetén:
Ez a fokozatos alulcsordulás (gradual underflow). Nem minden végrehajtási profil őrzi meg: egyes hardveres üzemmódok a subnormál eredményeket nullára cserélik.
Az IEEE 754 szerinti megvalósítás további értékeket is biztosít:
x == x hamis,
az x != x igaz; vizsgálatára mégis az isnan
való.signbit megkülönbözteti őket.Az ISO C17 önmagában nem ígéri minden ilyen érték és minden IEEE-kivételszabály támogatását. Különösen nem szabad abból kiindulni, hogy a nullával osztás minden C implementáción egyszerűen végtelent ad.
Mérési pipeline-ban általában külön adatminőségi szabály szükséges: elfogadunk-e végtelent, vagy minden nem véges értéket hibának tekintünk? A NaN nem helyettesíti automatikusan a hiányzó adat explicit jelölését.
A kettes számrendszerben végtelen törtként írható fel. Véges bináris reprezentáció ezért csak közelíti. Ugyanez igaz sok művelet pontos matematikai eredményére.
Legyen a pontos, a tárolt érték. Az abszolút és relatív hiba:
Legközelebbi értékre kerekítésnél, normál tartományban, túlcsordulás és alulcsordulás nélkül egy binary64 művelet szokásos hibamodellje:
Ez egyetlen megfelelően kerekített művelet hibakorlátja, nem egy
teljes algoritmusé. A DBL_EPSILON ebben a profilban
:
sem a legkisebb pozitív érték, sem univerzális összehasonlítási
küszöb.
Közel azonos mennyiségek kivonásakor a bemenetek hibája az eredményhez képest nagy lehet. Maga a kivonás akár pontos is lehet, miközben a különbség relatív hibája jelentős: a probléma a már pontatlan bemenetekből indul.
Numerikus összehasonlításra gyakori szerződés:
Az abszolút tolerancia a mennyiség mértékegységében értendő; a relatív tolerancia dimenzió nélküli. Nulla közelében az abszolút tag szükséges. A toleranciát a szenzor felbontása, az algoritmus hibája és az alkalmazás követelménye alapján választjuk, nem egy véletlenszerű „kis epsilonból”.
A következő függvény nemnegatív, véges késleltetésekre készült. Hibás érték vagy tolerancia esetén hamisat ad. A feltétel és a nemnegatív bemenetek megakadályozzák a különbség, illetve a relatív küszöb túlcsordulását. Nem ad át tulajdonjogot és nem tart fenn kölcsönzött hozzáférést.
#include <float.h>
#include <math.h>
#include <stdbool.h>
#include <stdio.h>
static bool latency_close(double a, double b,
double atol, double rtol)
{
if (!isfinite(a) || !isfinite(b) ||
!isfinite(atol) || !isfinite(rtol) ||
a < 0.0 || b < 0.0 || atol < 0.0 ||
rtol < 0.0 || rtol > 1.0) {
return false;
}
return fabs(a - b) <= fmax(atol, rtol * fmax(a, b));
}
int main(void)
{
double sum = 0.1 + 0.2;
double reference = 0.3;
printf("radix=%d, double precision=%d, eval=%d\n",
FLT_RADIX, DBL_MANT_DIG, FLT_EVAL_METHOD);
printf("sizes: %zu %zu %zu C_byte\n",
sizeof(float), sizeof(double), sizeof(long double));
printf("sum=%.*g, hex=%a\n",
DBL_DECIMAL_DIG, sum, sum);
printf("exact=%d, tolerant=%d\n",
sum == reference,
latency_close(sum, reference, 1e-12, 1e-9));
return 0;
}Javasolt, rögzített kiinduló fordítópárok: GCC 14.2.0 és Clang 19.1.7, glibc 2.40 mellett, a fenti x86-64 célprofilon. A parancsok külön laborhoz tartoznak; nem jelentenek elvégzett fordítást vagy tesztet.
gcc -std=c17 -O2 -Wall -Wextra -Wpedantic \
-march=x86-64 -fno-fast-math -ffp-contract=off \
numeric.c -lm -o numeric-gcc
clang -std=c17 -O2 -Wall -Wextra -Wpedantic \
-march=x86-64 -fno-fast-math -ffp-contract=off \
numeric.c -lm -o numeric-clangBinary64 és legközelebbi értékre kerekítés mellett a várt decimális
összeg 0.30000000000000004; a pontos összehasonlítás hamis,
a választott toleranciás összehasonlítás igaz. Ezek profilhoz kötött
várt eredmények.
A <math.h> műveletei között szerepel a
sqrt, log, fabs,
nextafter és fma. A megfelelő f
és l végződésű változatok általában float,
illetve long double értékekkel dolgoznak. A
fpclassify, isfinite, isnan és
signbit az értékek osztályozását segítik.
Hibakezelésnél a math_errhandling jelzi az
errno-alapú és/vagy lebegőpontos kivételjelzést. Nem helyes
minden függvényhívás után kizárólag az errno-t
figyelni.
A <fenv.h> a lebegőpontos környezetet teszi
elérhetővé:
fegetround, fesetround: kerekítési mód
lekérdezése és módosítása;feclearexcept, fetestexcept: támogatott
kivételflagek törlése és vizsgálata.A lebegőpontos kivételflag nem feltétlenül jelent processzmegszakítást vagy signalt. A támogatott módokat, makrókat és a függvények visszatérési értékét ellenőrizni kell.
ISO C17 mechanizmus: a
#pragma STDC FENV_ACCESS ON jelzi, hogy a program
megfigyeli vagy módosítja a környezetet. Fordítókorlát:
támogatása és optimalizálással való együttműködése verziófüggő; a
-frounding-math önmagában nem bizonyít teljes
támogatást.
A műveletösszevonás (contraction) szintén fontos. A külön szorzás és összeadás két kerekítést végezhet:
míg az összevont művelet egyet:
Az eredmények eltérhetnek. A
#pragma STDC FP_CONTRACT OFF ennek szabványos szabályozási
eszköze; tényleges támogatását ellenőrizni kell. A laborparancs ezért
külön -ffp-contract=off kapcsolót használ. Az explicit
fma(a,b,c) továbbra is egyszeri kerekítésű műveletet
kér.
A GCC/Clang -ffast-math kapcsolója nem pusztán gyorsabb
végrehajtást választ. Verzió- és célfüggő engedményeket kapcsolhat be:
átrendezheti a műveleteket, feltételezheti a NaN és infinity hiányát,
figyelmen kívül hagyhatja az előjeles nullát vagy a dinamikus
kerekítést. Egyes profilok a subnormál kezelést is módosítják. Emiatt
akár egy isfinite-alapú adatellenőrzés szerződése is
sérülhet.
A reprodukálhatósági jegyzőkönyv rögzítse a fordító és libc
verzióját, cél-ABI-t, kapcsolókat, kerekítési módot,
contraction-szabályt, subnormál kezelést és releváns
float.h értékeket. Ez szükséges, de a matematikai
könyvtárak eltérő közelítései miatt nem mindig elegendő a platformok
közötti bitazonossághoz.
A hexadecimális lebegőpontos literálban a p utáni kitevő
kettes alapú:
A 0x1.999999999999ap-4 a binary64-ben szokásos,
-hez
legközelebbi értéket írja le pontosan. Ez alkalmas regressziós tesztek
bemeneteinek rögzítésére.
A %a hexadecimális kiírás kettes alapú profilban
alapértelmezett pontossággal pontosan megkülönböztethető alakot ad.
Decimális visszaolvasáshoz használjuk például:
/* Részlet; C17, <float.h> és <stdio.h> szükséges. */
printf("%.*g\n", DBL_DECIMAL_DIG, value);A value itt double.
long double esetén LDBL_DECIMAL_DIG és
%.*Lg, illetve %La szükséges. A
printf variadikus hívásában a float
automatikusan double-re promótálódik.
A reprodukálható szöveghez a locale-t, a visszaolvasási eljárást és a
kerekítési környezetet is rögzíteni kell. A fenti teljes program nem
módosít locale-t, így a kezdeti C locale-t használja.
double pontosan tárolja a decimális
adatot.” Csak bizonyos értékeket; pontos elszámoláshoz gyakran
skálázott egész reprezentáció kell.== hibás.” Pontosan
reprezentálható vagy diszkrét állapotértékeknél helyes lehet. A
toleranciás közelség viszont nem tranzitív, ezért nem általános
egyenlőségi reláció.Magyarázó: mekkora a binary64 értékek távolsága közvetlenül másodperc fölött? Megőrizhető minden 100 ns-os eltérés?
Kódolási: egészítsd ki a példát tesztekkel. Ellenőrizd a kétféle nullát, egy negatív késleltetés elutasítását, az abszolút tolerancia szerepét és egy túl nagy relatív tolerancia elutasítását.
Diagnosztikai: legyen a = 1e16,
b = -1e16, c = 1.0. Binary64, legközelebbi
kerekítés és külön műveletek mellett miért különbözhet
(a + b) + c és a + (b + c)? Mit
következtethetsz egy fast-math fordításból?
Itt , ezért:
Nem őrizhető meg minden 100 ns-os eltérés. Ellenőrzésként a
nextafter(t, DBL_MAX) - t különbség vizsgálható
t = 0x1p32 mellett, a rögzített profilban.
Az eredeti C17-programhoz adható részlet:
#include <assert.h>
static void test_latency_close(void)
{
assert(latency_close(0.0, -0.0, 0.0, 0.0));
assert(!latency_close(-0.1, 0.1, 1.0, 0.0));
assert(latency_close(0.0, 5e-10, 1e-9, 0.0));
assert(!latency_close(0.0, 5e-10, 0.0, 1e-9));
assert(!latency_close(1.0, 1.0, 0.0, 2.0));
}Hívd meg a függvényt a main elején, és ne definiáld az
NDEBUG makrót. Az ellenőrzési feltétel: minden állítás
teljesül. A harmadik és negyedik teszt különíti el az abszolút és
relatív küszöb szerepét.
Az a + b pontosan nulla, így az első kifejezés eredménye
.
A
körüli binary64 lépésköz
;
a b + c félúton lévő pontos eredménye pároshoz kerekítéskor
visszakerekedik b-re. A második kifejezés így
.
Fast-math mellett az átrendezés megengedett lehet, ezért a zárójelezésből önmagában nem következik ez az eredménypár. Azonos kimenet sem bizonyítja a kapcsoló ártalmatlanságát: csak az adott fordítás megfigyelése.
A lebegőpontos pontosság a nagyságrendtől, reprezentációtól és műveleti sorrendtől függ. Megbízható mérési kódhoz tartomány- és toleranciaszerződés, különlegesérték-kezelés, valamint rögzített fordítási profil szükséges. A pontos kiírás láthatóvá teszi a tárolt értéket; az elveszett információt azonban sem több számjegy, sem későbbi típusbővítés nem állítja vissza.