// WIKI

Formati dati ed endianness

Come gli stessi byte diventano numeri diversi — endianness big/little, interi in complemento a due, float IEEE-754 single/double (segno, esponente con bias, mantissa) e i casi pratici nella lettura di registri e frame binari.

Pubblicato il Aggiornato il EndiannessIEEE 754HexData typesStrumentazione

Un buffer di byte non ha, di per sé, un significato numerico: lo acquista solo quando decidiamo come leggerlo — quanti byte raggruppare, in che ordine, con o senza segno, come intero o come float. Lo stesso 40 49 0F DB è un intero senza segno, un intero con segno o π in virgola mobile, a seconda della convenzione. Questo strumento mostra tutte le interpretazioni in parallelo; questa pagina spiega le regole che ci stanno dietro.

Endianness: big e little

Un valore più grande di un byte occupa più celle di memoria, e va deciso in che ordine. In big-endian il byte più significativo (MSB) viene per primo — lo stesso ordine in cui scriviamo le cifre di un numero. In little-endian viene per ultimo: il byte meno significativo apre la sequenza.

La differenza è concreta. La sequenza 01 00 letta come intero a 16 bit vale:

  • big-endian: 0x0100 = 256
  • little-endian: 0x0001 = 1

Stessi due byte, due numeri diversi. Nessuno dei due è “giusto” in assoluto: è una convenzione, e va concordata fra chi scrive e chi legge.

Nella pratica le architetture x86 e ARM (in configurazione tipica) sono little-endian. Molti protocolli di rete trasmettono in big-endian, tanto che il big-endian è chiamato anche network byte order; numerosi formati di file binari e datasheet di periferiche specificano esplicitamente l’ordine. Sbagliare endianness è uno degli errori più comuni — e più silenziosi — nel parsing di dati binari.

Il sintomo che ci si aspetta non è quello che arriva

Si sente dire che uno float letto con l’endianness sbagliata «diventa NaN». Sarebbe una fortuna, perché un NaN si nota al primo controllo. Provato su 200.000 valori con modulo da 10⁻² a 10⁴ — l’intervallo tipico di una misura — scambiando i quattro byte di un float32 si ottiene:

Cosa si ottiene Frequenza
un valore plausibile 51,69 %
modulo enorme (> 10²⁰) 24,07 %
modulo minuscolo (< 10⁻²⁰) 23,83 %
NaN 0,41 %
infinito o zero 0,00 %

Il NaN arriva quattro volte su mille. In più della metà dei casi si ottiene un numero dall’aria del tutto innocua, che passa qualunque controllo di validità e si propaga a valle. È il vero motivo per cui l’errore di endianness è «silenzioso»: non perché non lasci traccia, ma perché la traccia che lascia sembra un dato.

Su valori singoli si vede bene come il risultato non abbia nessuna regolarità:

Valore atteso Byte invertiti Word invertite (middle-endian)
1 4,6006e−41 2,2780e−41
3,14159 −9,6158e+9 2,0535e−29
0,1 −4,2949e+8 −1,0761e+8
25,5 7,3272e−41 2,3603e−41
1000 4,3861e−41 2,4565e−41
0,001 4,5343e+28 7,5487e−28
1000000 3,3478e−39 2,7818e−17

E c’è anche il middle-endian

L’ultima colonna della tabella è un terzo caso, che chi lavora con dispositivi industriali incontra prima o poi. Molti slave Modbus espongono un valore a 32 bit come due registri a 16 bit: ciascun registro è big-endian per specifica, ma l’ordine dei due registri non lo è sempre. Il risultato è un byte order 2 3 0 1 — né big né little, spesso chiamato middle-endian o byte-swapped — e nessuna delle due interpretazioni classiche lo decodifica.

È il caso in cui affiancare le interpretazioni non basta: se né big né little danno un valore sensato ma i due registri singoli sembrano plausibili, la risposta è quasi sempre l’ordine delle word. Lo strumento copre big e little, non il middle-endian: la soluzione pratica è scambiare a mano le due coppie di byte prima di incollarle.

Interi: con e senza segno, complemento a due

Un gruppo di N bit può essere letto come intero senza segno — un valore non negativo in [0, 2ᴺ−1] — oppure con segno. La codifica con segno universale è il complemento a due: il bit più significativo non si somma, si sottrae. Per N bit:

Il negativo di un numero si ottiene invertendo tutti i bit e sommando 1. Il vantaggio del complemento a due è che l’addizione e la sottrazione funzionano con lo stesso hardware degli interi senza segno: non serve gestire un “−0” separato.

Conseguenza pratica: gli stessi 8 bit 0xFF valgono −1 letti come int8 e 255 letti come uint8. 0x80 vale −128 con segno e 128 senza. Ecco perché un registro a 16 bit “che dovrebbe essere positivo” ma arriva come un numero enorme è quasi sempre un int letto come uint (o viceversa).

IEEE 754: segno, esponente, mantissa

I numeri in virgola mobile seguono lo standard IEEE 754. Un valore è codificato in tre campi: un bit di segno s, un esponente e memorizzato con un bias, e una mantissa m (le cifre significative). Per i valori normali:

I due formati comuni:

bit segno bit esponente bit mantissa bias
single (float32) 1 8 23 127
double (float64) 1 11 52 1023

Due dettagli rendono l’IEEE 754 efficiente:

  • bias dell’esponente: l’esponente è memorizzato come numero non negativo, da cui si sottrae il bias per ottenere quello reale. Così un esponente grezzo di 128 nel single vale 128 − 127 = +1, e uno di 126 vale −1. Permette di rappresentare esponenti positivi e negativi senza un secondo bit di segno.
  • bit nascosto: per i numeri normali la mantissa ha un 1 implicito davanti alla virgola, che non viene memorizzato. Così i 23 bit di mantissa del single danno di fatto 24 bit di precisione.

I valori limite dell’esponente sono riservati:

  • esponente tutto a 0: lo zero (mantissa nulla, e ±0 distinti dal bit di segno) e i subnormali (mantissa non nulla, senza l’1 implicito — colmano il gap attorno allo zero).
  • esponente tutto a 1: infinito (mantissa nulla) e NaN (mantissa non nulla — risultati indefiniti come 0/0).

Lo strumento decompone i float visualizzando i tre campi come gruppi di bit colorati, mostra l’esponente grezzo, il bias e quello effettivo, ricostruisce il valore dai campi e ne dichiara la classe (normale, subnormale, zero, infinito, NaN).

Un esempio: 40 49 0F DB

I quattro byte 40 49 0F DB, letti come float32 big-endian, valgono 0x40490FDB. Il bit di segno è 0 (positivo); gli 8 bit di esponente sono 0x80 = 128, da cui l’esponente reale 128 − 127 = +1; i 23 bit di mantissa, con l’1 implicito, danno 1,5708. Il valore è quindi (+1)·1,5708·2¹ = 3,14159…, cioè π. Letti in little-endian gli stessi byte diventano 0xDB0F4940: un float del tutto diverso, per giunta negativo (il bit di segno cade ora nel byte 0xDB). Stessa sequenza, risultato irriconoscibile — la dimostrazione pratica di quanto l’endianness cambi tutto.

Quanta precisione c’è davvero

I due formati hanno un numero fisso di bit di mantissa, quindi il passo fra due numeri rappresentabili consecutivi cresce col valore, ed è la ragione della maggior parte delle sorprese numeriche.

Grandezza Passo float32 Passo float64
1 1,19e−7 2,22e−16
100 7,63e−6 1,42e−14
10⁴ 9,77e−4 1,82e−12
10⁶ 6,25e−2 1,16e−10
1,68·10⁷ 2 3,73e−9
10⁹ 64 1,19e−7
10¹⁵ 3,36e+7 0,125

Il float32 ha 24 bit effettivi di precisione, quindi da 1,68·10⁷ in su il passo supera l’unità: due interi consecutivi non sono più distinguibili. Il confine esatto è 2²⁴ = 16.777.216, oltre il quale 16777216 e 16777217 sono lo stesso float32. Per il double il confine è 2⁵³ = 9.007.199.254.740.992.

Da qui due conseguenze operative frequenti:

  • un contatore di millisecondi in float32 perde la risoluzione al millisecondo dopo circa 4,7 ore (16,7 milioni di ms) — motivo per cui i timestamp non si tengono in float32;
  • un identificativo a 64 bit non sopravvive al passaggio in un double, che è il tipo numerico di JavaScript (Number è IEEE-754 binary64). JSON definisce invece una sintassi numerica decimale senza imporre una rappresentazione interna, ma nella pratica va trattato come binary64, perché è quello che usano le implementazioni più diffuse: sopra 2⁵³ gli ID vengono silenziosamente arrotondati. Per questo gli ID grandi si trasmettono come stringhe.
Formato Min subnormale Min normale Max ε (eps) Cifre affidabili / round-trip
float32 1,40e−45 1,18e−38 3,40e+38 1,19e−7 6 / 9
float64 4,94e−324 2,23e−308 1,80e+308 2,22e−16 15 / 17

Le «cifre affidabili» sono quelle che si possono leggere e riscrivere senza perdere niente (6 per il single, 15 per il double); le «cifre di round-trip» sono quelle che servono a garantire che il valore riletto sia bit per bit lo stesso (9 e 17). Sono due numeri diversi e servono a due scopi diversi: il primo per stampare, il secondo per serializzare.

L’errore si accumula

Il caso peggiore non è il singolo arrotondamento ma la somma di molti. Sommando 0,01 un milione di volte, dove il risultato esatto è 10.000:

  • in float64: 10.000,000000172 — errore 1,7·10⁻⁷, trascurabile;
  • in float32: 9.865,22 — errore di 134,78, cioè il −1,35 %.

Il float32 sbaglia di oltre l’uno percento non per un bug ma perché, quando l’accumulatore ha superato il migliaio, il passo di rappresentazione è diventato più grande dell’incremento che si sta sommando: una parte degli addendi viene perduta. È il motivo per cui gli integratori, i filtri IIR e i totalizzatori si scrivono in doppia precisione o in virgola fissa, anche quando i dati in ingresso sono float32.

E il classico, che qui ha una spiegazione e non solo una constatazione: 0,1 + 0,2 dà 0,30000000000000004 in double perché né 0,1 né 0,2 hanno una rappresentazione binaria finita — come 1/3 non ce l’ha in decimale. In float32 lo stesso conto dà 0,30000001192092896.

Casi pratici

Sapere leggere i byte è quotidiano in strumentazione e protocolli:

  • Lettura di registri: un datasheet definisce un registro a 16 o 32 bit, con segno o senza, e un certo endian. Per interpretarlo bisogna applicare esattamente quella convenzione.
  • Parsing di frame binari: Modbus, CAN, un payload UDP, un formato di file proprietario — ogni campo ha una larghezza, un tipo e un ordine dei byte. Il primo passo del debug di un parser è capire quale combinazione tipo+endian produce il valore atteso.
  • Round-trip e scambio dati: un float salvato da un microcontrollore e riletto sul PC arriva “sbagliato” se le due estremità non concordano sull’endianness. Vedere le interpretazioni affiancate rende immediato individuare la combinazione corretta.

La modalità inversa dello strumento fa il percorso opposto: dato un valore, un tipo e un endian, restituisce i byte esadecimali — utile per costruire un frame di test o verificare un round-trip.

Riferimenti

  • IEEE 754 — standard per l’aritmetica in virgola mobile (formati single/double, casi speciali, arrotondamento).
  • Strumento correlato — l’ispettore di formati dati ed endianness mette in pratica questa pagina: incolla i byte, scegli l’offset e leggi tutte le interpretazioni in big e little endian, con la decomposizione IEEE-754.

Ultimo aggiornamento: · Trovato un errore o un dato superato? Segnalacelo

← Torna all’indice Wiki

Un progetto simile?

Acustica, embedded, strumenti di calcolo: se hai un caso d’uso vicino, parliamone.