Determinare le Caratteristiche dei Numeri in Virgola Mobile in Linguaggio C

Per chi scrive algoritmi e programmi di calcolo numerico in linguaggio C, è fondamentale conoscere le caratteristiche dei numeri in virgola mobile supportati dal compilatore utilizzato. Queste caratteristiche includono la precisione, l'intervallo di valori rappresentabili e il comportamento delle operazioni aritmetiche.

Sebbene allo sviluppatore comune spesso non interessino questi dettagli, per chi si occupa di calcolo numerico essi sono cruciali per garantire l'accuratezza e l'affidabilità dei risultati ottenuti.

Prima di addentrarci nelle funzionalità matematiche offerte dal linguaggio C e dalla sua libreria standard, in questa lezione vedremo come determinare le caratteristiche dei numeri in virgola mobile supportati dal compilatore C in uso.

Concetti Chiave
  • Per determinare se un compilatore C supporta i numeri in virgola mobile, possiamo verificare la presenza della macro __STDC_IEC_559__.
  • Le caratteristiche specifiche dei numeri in virgola mobile, come la precisione e l'intervallo di valori rappresentabili, possono essere ottenute tramite le macro definite nel file di intestazione <float.h>.
  • Il comportamento della valutazione delle espressioni in virgola mobile può essere determinato utilizzando la macro FLT_EVAL_METHOD, che indica il metodo di valutazione adottato dal compilatore.

Determinare se un compilatore C supporta i numeri in virgola mobile

Quando abbiamo studiato i numeri in virgola mobile abbiamo visto i tipi che il linguaggio C mette a disposizione per rappresentare questi numeri: float, double e long double. Ognuno di questi tipi ha delle caratteristiche specifiche in termini di precisione e intervallo di valori rappresentabili.

Il fatto è che lo standard del linguaggio C prevede due cose:

  1. Non è necessario che il compilatore implementi tutti e tre i tipi in virgola mobile. Un compilatore potrebbe non supportarli affatto.

    Si pensi, ad esempio, ai compilatori C per i microcontrollori più semplici (ad esempio Microchip PIC18, Atmel AVR e STMicroelectronics STM32), che spesso non supportano i numeri in virgola mobile per motivi di spazio e prestazioni. Anzi, nella maggioranza dei casi, tali microcontrollori non dispongono di un'unità di calcolo in virgola mobile (FPU, Floating Point Unit).

    In questi casi, quello che accade è che il compilatore semplicemente non supporta i numeri in virgola mobile e non compila il codice che li utilizza, oppure emula le operazioni in virgola mobile tramite codice software, il che può essere molto lento.

  2. Non è mandatorio che, seppure i tipi in virgola mobile siano supportati, essi debbano rispettare lo standard IEEE 754.

    Lo standard IEEE 754 definisce come devono essere rappresentati i numeri in virgola mobile, quali operazioni devono essere supportate e come devono comportarsi in caso di errori (come overflow, underflow, divisione per zero, ecc.). Tuttavia, un compilatore C potrebbe scegliere di implementare i numeri in virgola mobile in modo diverso.

In generale, non esiste un modo universale per sapere se un compilatore C supporta i numeri in virgola mobile. Tuttavia, è possibile almeno verificare che essi rispettino lo standard IEEE 754 (chiamato anche standard IEC 60559) utilizzando la macro __STDC_IEC_559__.

Ad esempio, il seguente codice verifica se il compilatore supporta lo standard IEEE 754:

#include <stdio.h>

int main() {
    #ifdef __STDC_IEC_559__
        printf("Il compilatore supporta lo standard IEEE 754.\n");
    #else
        printf("Il compilatore NON supporta lo standard IEEE 754.\n");
    #endif
    return 0;
}

Determinare le caratteristiche dei numeri in virgola mobile

Detto questo, una volta che abbiamo stabilito che il compilatore supporta i numeri in virgola mobile, è utile conoscere le loro caratteristiche specifiche, come la precisione e l'intervallo di valori rappresentabili. Per fare ciò, possiamo utilizzare il file di intestazione <float.h>, che definisce una serie di macro che forniscono queste informazioni.

Queste macro hanno uno di tre possibili prefissi, a seconda del tipo di virgola mobile a cui si riferiscono:

  • FLT_ per il tipo float
  • DBL_ per il tipo double
  • LDBL_ per il tipo long double

Nella tabbella seguente sono elencate alcune delle macro più comuni definite in <float.h>:

Macro Descrizione
FLT_RADIX Specifica la base della rappresentazione dell'esponente (tipicamente 2 per i numeri binari). Questa macro è comune a tutti i tipi in virgola mobile.
FLT_MANT_DIG, DBL_MANT_DIG, LDBL_MANT_DIG Numero di cifre significative (bit) nella mantissa per float, double e long double, rispettivamente. Questo numero indica il numero di cifre nella base specificata da FLT_RADIX che possono essere rappresentate con precisione e non in base 10.
FLT_MIN_EXP, DBL_MIN_EXP, LDBL_MIN_EXP L'esponente minimo (in base FLT_RADIX) per i tipi float, double e long double, rispettivamente.
FLT_MAX_EXP, DBL_MAX_EXP, LDBL_MAX_EXP L'esponente massimo (in base FLT_RADIX) per i tipi float, double e long double, rispettivamente.
FLT_MIN, DBL_MIN, LDBL_MIN Il più piccolo numero positivo normalizzato rappresentabile per i tipi float, double e long double, rispettivamente.
FLT_MAX, DBL_MAX, LDBL_MAX Il più grande numero rappresentabile per i tipi float, double e long double, rispettivamente.
FLT_EPSILON, DBL_EPSILON, LDBL_EPSILON Il cosiddetto epsilon di macchina, che rappresenta la differenza più piccola tra 1 e il successivo numero rappresentabile per i tipi float, double e long double, rispettivamente.
FLT_DIG, DBL_DIG, LDBL_DIG Numero di cifre decimali che possono essere rappresentate con precisione per i tipi float, double e long double, rispettivamente.
Tabella 1: Tabella delle principali macro definite in float.h

Tra le macro elencate, alcune sono particolarmente utili per comprendere la precisione e l'intervallo dei numeri in virgola mobile:

  • FLT_MANT_DIG, DBL_MANT_DIG, LDBL_MANT_DIG: Queste macro indicano quante cifre significative possono essere rappresentate con precisione. Ad esempio, se FLT_MANT_DIG è 24, significa che un numero di tipo float può rappresentare con precisione fino a 24 cifre binarie nella sua mantissa.
  • FLT_EPSILON, DBL_EPSILON, LDBL_EPSILON: Rappresenta il limite superiore dell'errore relativo dovuto all'arrotondamento quando si eseguono operazioni in virgola mobile. Ad esempio, se FLT_EPSILON è 1.19209290e-07, significa che la differenza tra 1 e il successivo numero rappresentabile è circa 1.19 x 10^-7 per i numeri di tipo float.

Proviamo a scrivere un semplice programma che stampa alcune di queste caratteristiche per i tipi float, double e long double:

#include <stdio.h>
#include <float.h>

int main() {
    printf("Caratteristiche dei numeri in virgola mobile:\n\n");

    printf("Tipo float:\n");
    printf("  Base dell'esponente: %d\n", FLT_RADIX);
    printf("  Cifre significative: %d\n", FLT_MANT_DIG);
    printf("  Min esponente: %d\n", FLT_MIN_EXP);
    printf("  Max esponente: %d\n", FLT_MAX_EXP);
    printf("  Min positivo normalizzato: %e\n", FLT_MIN);
    printf("  Max rappresentabile: %e\n", FLT_MAX);
    printf("  Epsilon di macchina: %e\n", FLT_EPSILON);
    printf("  Cifre decimali rappresentabili con precisione: %d\n\n", FLT_DIG);

    printf("Tipo double:\n");
    printf("  Base dell'esponente: %d\n", FLT_RADIX);
    printf("  Cifre significative: %d\n", DBL_MANT_DIG);
    printf("  Min esponente: %d\n", DBL_MIN_EXP);
    printf("  Max esponente: %d\n", DBL_MAX_EXP);
    printf("  Min positivo normalizzato: %e\n", DBL_MIN);
    printf("  Max rappresentabile: %e\n", DBL_MAX);
    printf("  Epsilon di macchina: %e\n", DBL_EPSILON);
    printf("  Cifre decimali rappresentabili con precisione: %d\n\n", DBL_DIG);

    printf("Tipo long double:\n");
    printf("  Base dell'esponente: %d\n", FLT_RADIX);
    printf("  Cifre significative: %d\n", LDBL_MANT_DIG);
    printf("  Min esponente: %d\n", LDBL_MIN_EXP);
    printf("  Max esponente: %d\n", LDBL_MAX_EXP);
    printf("  Min positivo normalizzato: %Le\n", LDBL_MIN);
    printf("  Max rappresentabile: %Le\n", LDBL_MAX);
    printf("  Epsilon di macchina: %Le\n", LDBL_EPSILON);
    printf("  Cifre decimali rappresentabili con precisione: %d\n\n", LDBL_DIG);

    return 0;
}

Provando a compilare ed eseguire questo programma, otterremo un output che ci fornisce una panoramica delle caratteristiche dei numeri in virgola mobile supportati dal nostro compilatore C. Ad esempio, su un sistema linux con GCC che gira su una macchina con architettura x86_64, l'output potrebbe essere simile al seguente:

Caratteristiche dei numeri in virgola mobile:

Tipo float:
  Base dell'esponente: 2
  Cifre significative: 24
  Min esponente: -125
  Max esponente: 128
  Min positivo normalizzato: 1.175494e-38
  Max rappresentabile: 3.402823e+38
  Epsilon di macchina: 1.192093e-07
  Cifre decimali rappresentabili con precisione: 6

Tipo double:
  Base dell'esponente: 2
  Cifre significative: 53
  Min esponente: -1021
  Max esponente: 1024
  Min positivo normalizzato: 2.225074e-308
  Max rappresentabile: 1.797693e+308
  Epsilon di macchina: 2.220446e-16
  Cifre decimali rappresentabili con precisione: 15

Tipo long double:
  Base dell'esponente: 2
  Cifre significative: 64
  Min esponente: -16381
  Max esponente: 16384
  Min positivo normalizzato: 3.362103e-4932
  Max rappresentabile: 1.189731e+4932
  Epsilon di macchina: 1.084202e-19
  Cifre decimali rappresentabili con precisione: 18

Comportamento della Valutazione in Virgola Mobile

Un dettaglio fondamentale da considerare quando si lavora con i numeri in virgola mobile in C è il comportamento della valutazione delle espressioni.

Infatti, lo standard del linguaggio C consente ai compilatori di utilizzare una precisione maggiore durante la valutazione delle espressioni in virgola mobile rispetto a quella dei tipi di dati coinvolti. Questo significa che, ad esempio, un'espressione che coinvolge variabili di tipo float potrebbe essere valutata con una precisione equivalente a quella di un double o addirittura di un long double. Questo comportamento può portare a risultati inattesi, specialmente quando si confrontano i risultati di operazioni in virgola mobile con valori attesi.

Per chi sviluppa algoritmi di calcolo numerico, è importante essere consapevoli di questo aspetto. Pertanto, lo standard C fornisce un'ulteriore macro, FLT_EVAL_METHOD, definita in <float.h>, che indica il metodo di valutazione utilizzato dal compilatore:

  • 0: Le espressioni in virgola mobile sono valutate con la precisione del tipo di dato coinvolto.
  • 1: Le espressioni in virgola mobile sono valutate con la precisione di double.
  • 2: Le espressioni in virgola mobile sono valutate con la precisione di long double.
  • -1: Il metodo di valutazione non è specificato.

Ad esempio, possiamo scrivere un semplice programma per stampare il valore di FLT_EVAL_METHOD:

#include <stdio.h>
#include <float.h>

int main() {
    printf("Metodo di valutazione delle espressioni in virgola mobile: %d\n",
           FLT_EVAL_METHOD);
    return 0;
}

Compilando ed eseguendo questo programma, otterremo un output che ci indica il metodo di valutazione delle espressioni in virgola mobile utilizzato dal nostro compilatore C. Ad esempio, su un sistema linux con GCC 15 che gira su una macchina con architettura x86_64, l'output potrebbe essere:

Metodo di valutazione delle espressioni in virgola mobile: 0

Il che vuol dire che le espressioni in virgola mobile sono valutate con la precisione del tipo di dato coinvolto.