L'Ambiente di Esecuzione Matematica in Linguaggio C
Lo standard IEEE 754 rappresenta il formato più diffuso per rappresentare e manipolare i numeri in virgola mobile. Questo standard prende anche il nome di IEC 60559.
Sebbene il linguaggio C e la sua libreria matematica standard astraggano l'accesso all'hardware e alle funzionalità dei processori che ne fanno uso, può accadere che alcune applicazioni (specialmente quelle che fanno grande uso di calcoli numerici) debbano accedere in maniera diretta alle funzionalità di basso livello. Tali funzionalità prendono collettivamente il nome di Ambiente di Esecuzione Matematica o, in inglese, Floating Point Environment.
Per accedere all'environment, la libreria standard del C mette a disposizione un file header chiamato <fenv.h>. In questa lezione vedremo come usare e gestire l'ambiente di esecuzione.
- L'ambiente di esecuzione matematica controlla il comportamento delle operazioni in virgola mobile.
- Esso è composto dai flag di stato in virgola mobile e dalle modalità di controllo.
- La libreria standard del C fornisce delle funzionalità per accedere e gestire l'ambiente di esecuzione matematica.
- Tali funzionalità sono dichiarate nell'header
<fenv.h>. - Si tratta di funzionalità avanzata che richiedono l'abilitazione esplicita dell'accesso all'ambiente di esecuzione matematica e che vengono usate principalmente in applicazioni numeriche ad alte prestazioni.
Flag di stato Floating Point e modalità di Controllo
Un Flag di stato Floating Point è una variabile di sistema che viene impostata a vero quando si verifica un errore in virgola mobile. Spesso, quando il processore sottostante implementa le operazioni in virgola mobile in hardware, si tratta di un vero e proprio registro del processore.
Nello standard IEEE 754 sono definiti cinque tipi di errori in virgola mobile:
- overflow;
- underflow;
- divisione per zero (division by zero);
- operazione invalida (invalid operation), quando il risultato di un'operazione è NaN (Not a Number);
- risultato non esatto (inexact), quando il risultato di un'operazione è stato approssimato.
A ciascuno di questi errori corrisponde un flag di stato che può essere vero o falso.
L'header <fenv.h> della libreria standard definisce un tipo particolare, fexcept_t, che può essere adoperato per lavorare con i flag di stato dell'ambiente. Un oggetto di tipo fexcept_t rappresenta il valore collettivo di tutti i flag di stato in virgola mobile. Sebbene esso possa essere un intero in cui ogni bit rappresenta un flag, la sua implementazione è lasciata al compilatore e alla piattaforma secondo lo standard C.
Un altro concetto importante nell'ambiente di esecuzione matematica è quello delle modalità di controllo (floating point control modes). Una modalità di controllo è una variabile di sistema che determina il comportamento futuro delle operazioni in virgola mobile.
Una modalità di controllo comune è la modalità di arrotondamento (rounding mode), che determina come i risultati delle operazioni in virgola mobile vengono arrotondati quando non possono essere rappresentati esattamente. Lo standard IEEE 754 definisce quattro modalità di arrotondamento:
- Round toward nearest: arrotonda al numero più vicino. In caso di parità, arrotonda al numero con il numero il cui bit meno significativo è zero (round half to even).
- Round toward zero: arrotonda verso zero (troncamento).
- Round toward positive infinity: arrotonda verso l'infinito positivo (ceiling).
- Round toward negative infinity: arrotonda verso l'infinito negativo (floor).
Di default, lo standard IEEE 754 specifica che la modalità di arrotondamento deve essere "round toward nearest".
Alcune implementazioni, anche hardware, possono supportare altre modalità di controllo:
- Precisione di Arrotondamento: determina la precisione con cui i risultati in virgola mobile vengono arrotondati (ad esempio, 24 bit per
float, 53 bit perdouble, ecc.). - Gestione degli Errori: determina come il sistema risponde quando si verifica un errore in virgola mobile (ad esempio, generando un'eccezione, ignorando l'errore, ecc.).
Ambiente di Esecuzione Matematica
L'Ambiente di Esecuzione Matematica o Floating Point Environment è l'unione dei flag di stato in virgola mobile e delle modalità di controllo. Esso rappresenta lo stato corrente delle operazioni in virgola mobile e può essere manipolato tramite le funzioni definite nell'header <fenv.h> della libreria standard del C.
Per rappresentare l'ambiente di esecuzione matematica, l'header <fenv.h> definisce un tipo chiamato fenv_t. Un oggetto di tipo fenv_t rappresenta lo stato completo dell'ambiente di esecuzione matematica, inclusi tutti i flag di stato e le modalità di controllo.
Per manipolare l'ambiente di esecuzione matematica, l'header <fenv.h> definisce alcune macro:
| Nome Macro | Valore | Descrizione |
|---|---|---|
FE_DIVBYZERO |
Costante intera | Rappresenta il flag di stato per la divisione per zero. |
FE_INEXACT |
Costante intera | Rappresenta il flag di stato per il risultato non esatto. |
FE_INVALID |
Costante intera | Rappresenta il flag di stato per l'operazione invalida. |
FE_OVERFLOW |
Costante intera | Rappresenta il flag di stato per l'overflow. |
FE_UNDERFLOW |
Costante intera | Rappresenta il flag di stato per l'underflow. |
FE_ALL_EXCEPT |
Costante intera | Rappresenta tutti i flag di stato in virgola mobile. |
| Nome Macro | Valore | Descrizione |
|---|---|---|
FE_TONEAREST |
Costante intera | Rappresenta la modalità di arrotondamento "round toward nearest". |
FE_DOWNWARD |
Costante intera | Rappresenta la modalità di arrotondamento "round toward negative infinity". |
FE_UPWARD |
Costante intera | Rappresenta la modalità di arrotondamento "round toward positive infinity". |
FE_TOWARDZERO |
Costante intera | Rappresenta la modalità di arrotondamento "round toward zero". |
Le macro che rappresentano i flag di stato hanno dei valori distinti che possono essere combinati usando l'operatore bitwise OR (|). Ad esempio, FE_DIVBYZERO | FE_OVERFLOW rappresenta entrambi i flag di stato per la divisione per zero e l'overflow.
La macro FE_ALL_EXCEPT rappresenta l'OR bit a bit di tutti i flag di stato definiti.
Secondo lo standard C, non è garantito che tutte le macro sopra definite siano presenti nell'implementazione. Per verificare quali macro sono supportate, bisogna controllare il valore di FE_ALL_EXCEPT. Se una macro non è supportata, il suo bit corrispondente in FE_ALL_EXCEPT sarà zero. Se la macro vale zero, significa che nessuna macro di flag di stato è supportata.
Le macro che rappresentano le modalità di arrotondamento sono mutualmente esclusive: solo una di esse può essere attiva in un dato momento. La modalità di arrotondamento corrente può essere ottenuta usando la funzione fegetround(), mentre può essere modificata usando la funzione fesetround(). Vedremo dopo come usare queste funzioni.
Un'importante macro definita nell'header <fenv.h> è FE_DFL_ENV, che rappresenta l'ambiente di esecuzione matematica di default. Quando si parla di default, si intende lo stato iniziale dell'ambiente di esecuzione matematica quando un programma in C inizia la sua esecuzione. Infatti, ogni programma in C ha il proprio ambiente di esecuzione matematica, che viene inizializzato al valore di FE_DFL_ENV all'inizio del programma.
Abilitare l'accesso all'Ambiente di Esecuzione Matematica
Normalmente, l'accesso all'ambiente di esecuzione matematica è disabilitato per motivi di prestazioni.
Per poter accedervi, l'header <fenv.h> definisce una direttiva #pragma chiamata FENV_ACCESS che si può usare per segnalare al compilatore che il programma necessita di accedere all'ambiente di esecuzione matematica.
Per poterla adoperare, la sintassi è la seguente:
#include <fenv.h>
#pragma STDC FENV_ACCESS valore
dove valore può essere ON, OFF o DEFAULT.
Quando valore è ON, si abilita l'accesso all'ambiente di esecuzione matematica. Quando valore è OFF, si disabilita l'accesso. Quando valore è DEFAULT, si ripristina il comportamento predefinito del compilatore che potrebbe essere di abilitare o disabilitare l'accesso a seconda dell'implementazione.
La durata dell'effetto della direttiva #pragma FENV_ACCESS dipende dal punto in cui essa viene inserita nel codice sorgente. Essa può essere inserita solo in tre punti:
-
In un file sorgente, a livello globale al di fuori di qualsiasi funzione.
In questo caso, la direttiva ha effetto per tutto il file sorgente a partire dalla sua posizione. Oppure finché non viene incontrata un'altra direttiva
#pragma FENV_ACCESSche ne modifica il valore. In ogni caso fino alla fine del file sorgente in cui è stata inserita. -
All'inizio di un blocco di codice.
In questo caso, la direttiva ha effetto per tutto il blocco di codice in cui è stata inserita. Oppure finché non viene incontrata un'altra direttiva
#pragma FENV_ACCESSche ne modifica il valore. In ogni caso fino alla fine del blocco di codice in cui è stata inserita.Ad esempio, può essere inserita all'inizio del corpo di una funzione per abilitare o disabilitare l'accesso all'ambiente di esecuzione matematica per tutta la durata della funzione:
#include <fenv.h> double funzione_esempio(double x) { #pragma STDC FENV_ACCESS ON // Corpo della funzione con accesso // all'ambiente di esecuzione matematica abilitato // ... } double altra_funzione(double y) { #pragma STDC FENV_ACCESS OFF // Corpo della funzione con accesso // all'ambiente di esecuzione matematica disabilitato // ... } double funzione_default(double z) { // Corpo della funzione con accesso // all'ambiente di esecuzione matematica // secondo il comportamento predefinito del compilatore // ... }Al termine del blocco di codice, l'accesso all'ambiente di esecuzione matematica ritorna al valore precedente a quello impostato dalla direttiva
#pragma FENV_ACCESS.
Ovviamente, è responsabilità del programmatore assicurarsi che l'accesso all'ambiente di esecuzione matematica sia abilitato quando si usano le funzioni che lo manipolano. Se l'accesso è disabilitato, il comportamento di tali funzioni è indefinito secondo lo standard C.
L'utilizzo tipico della direttiva #pragma FENV_ACCESS è quello di abilitarla all'inizio di una funzione che necessita di accedere all'ambiente di esecuzione matematica, e disabilitarla alla fine della funzione. In questo modo, si minimizza l'impatto sulle prestazioni del programma, limitando l'accesso solo alle parti del codice che ne hanno effettivamente bisogno.
#include <fenv.h>
double calcola_valore(double x) {
#pragma STDC FENV_ACCESS ON
// Corpo della funzione che accede
// all'ambiente di esecuzione matematica
// ...
}
La funzione calcola_valore ha l'accesso abilitato, quindi può verificare la presenza di errori in virgola mobile e modificare le modalità di arrotondamento secondo necessità.
Un altro dettaglio importante è che quando si passa da una regione di codice con accesso OFF a una con accesso ON, lo stato dei flag è indeterminato fino a quando non viene eseguita un'operazione in virgola mobile.
Funzioni per la gestione degli errori in virgola mobile
L'header <fenv.h> della libreria standard del C cinque funzioni per gestire i flag di stato in virgola mobile. Ognuna di tali funzioni prende in ingresso una maschera di flag di stato, che può essere ottenuta combinando le macro definite in precedenza usando l'operatore bitwise OR (|).
-
int feclearexcept(int excepts);Questa funzione resetta (imposta a falso) i flag di stato specificati dalla maschera
excepts. Restituisce0in caso di successo, oppure un valore diverso da zero in caso di errore. -
int feraiseexcept(int excepts);Questa funzione imposta (a vero) i flag di stato specificati dalla maschera
excepts. Restituisce0in caso di successo, oppure un valore diverso da zero in caso di errore. -
int fetestexcept(int excepts);Questa funzione verifica lo stato dei flag di stato specificati dalla maschera
excepts. Restituisce una maschera di bit in cui ogni bit corrispondente a un flag di stato specificato inexceptsè impostato a1se il flag è vero, oppure a0se il flag è falso.Ad esempio, per verificare se si è verificato un overflow o un underflow, si può usare il seguente codice:
#include <fenv.h> // Abilita l'accesso all'ambiente di esecuzione matematica #pragma STDC FENV_ACCESS ON // Esegui un'operazione in virgola mobile // che potrebbe causare un overflow o un underflow double risultato = operazione_in_virgola_mobile(); // Verifica se si è verificato un overflow if (fetestexcept(FE_OVERFLOW)) { // Gestisci l'overflow } else if (fetestexcept(FE_UNDERFLOW)) { // Gestisci l'underflow } -
int fegetexceptflag(fexcept_t *flagp, int excepts);Questa funzione salva lo stato dei flag di stato specificati dalla maschera
exceptsnell'oggettoflagpdi tipofexcept_t. Restituisce0in caso di successo, oppure un valore diverso da zero in caso di errore. -
int fesetexceptflag(const fexcept_t *flagp, int excepts);Questa funzione ripristina lo stato dei flag di stato specificati dalla maschera
exceptsusando l'oggettoflagpdi tipofexcept_t. Restituisce0in caso di successo, oppure un valore diverso da zero in caso di errore.L'oggetto
flagpdeve essere stato precedentemente ottenuto usando la funzionefegetexceptflag().
Funzioni per la gestione delle modalità di arrotondamento
L'header <fenv.h> della libreria standard del C definisce due funzioni per gestire le modalità di arrotondamento in virgola mobile:
-
int fegetround(void);Questa funzione restituisce la modalità di arrotondamento corrente come una delle macro definite in precedenza (
FE_TONEAREST,FE_DOWNWARD,FE_UPWARD,FE_TOWARDZERO). Se si verifica un errore, restituisce un valore diverso da queste macro. -
int fesetround(int round);Questa funzione imposta la modalità di arrotondamento corrente usando la macro
round, che deve essere una delle macro definite in precedenza (FE_TONEAREST,FE_DOWNWARD,FE_UPWARD,FE_TOWARDZERO). Restituisce0in caso di successo, oppure un valore diverso da zero in caso di errore.
Oltre a queste due funzioni, l'header <float.h> della libreria standard del C definisce una macro chiamata FLT_ROUNDS che rappresenta la modalità di arrotondamento corrente come un intero:
| Valore | Modalità di Arrotondamento |
|---|---|
| -1 | Indeterminata |
| 0 | Round toward zero |
| 1 | Round to nearest |
| 2 | Round toward positive infinity |
| 3 | Round toward negative infinity |
Funzioni per la gestione dell'Ambiente di Esecuzione Matematica
L'header <fenv.h> della libreria standard del C definisce quattro funzioni per gestire l'ambiente di esecuzione matematica:
-
int fegetenv(fenv_t *envp);Questa funzione salva l'ambiente di esecuzione matematica corrente nell'oggetto
envpdi tipofenv_t. Restituisce0in caso di successo, oppure un valore diverso da zero in caso di errore. -
int fesetenv(const fenv_t *envp);Questa funzione ripristina l'ambiente di esecuzione matematica usando l'oggetto
envpdi tipofenv_t. Restituisce0in caso di successo, oppure un valore diverso da zero in caso di errore.L'oggetto
envpdeve essere stato precedentemente ottenuto usando la funzionefegetenv()oppure può essereFE_DFL_ENVper ripristinare l'ambiente di esecuzione matematica di default. -
int feholdexcept(fenv_t *envp);Questa funzione svolge tre operazioni:
- Salva l'ambiente di esecuzione matematica corrente nell'oggetto
envpdi tipofenv_t. - Resetta tutti i flag di stato in virgola mobile.
- Imposta la modalità di controllo a non interrompere (ossia, le operazioni in virgola mobile non generano eccezioni).
Restituisce
0in caso di successo, oppure un valore diverso da zero in caso di errore. - Salva l'ambiente di esecuzione matematica corrente nell'oggetto
-
int feupdateenv(const fenv_t *envp);Questa funzione svolge tre operazioni:
- Salva i flag di stato in virgola mobile correnti in una variabile temporanea.
- Ripristina l'ambiente di esecuzione matematica usando l'oggetto
envpdi tipofenv_t. - Lancia le eccezioni per tutti i flag di stato che erano veri nella variabile temporanea.
Restituisce
0in caso di successo, oppure un valore diverso da zero in caso di errore.
Esempio di utilizzo dell'Ambiente di Esecuzione Matematica
Ecco un esempio di utilizzo dell'ambiente di esecuzione matematica in C:
#include <stdio.h>
#include <fenv.h>
#include <math.h>
#pragma STDC FENV_ACCESS ON
int main() {
// Esegui un'operazione in virgola mobile
double x = 1.0e308;
double y = 1.0e10;
double risultato = x * y; // Potrebbe causare overflow
// Verifica se si è verificato un overflow
if (fetestexcept(FE_OVERFLOW)) {
printf("Overflow rilevato!\n");
// Gestisci l'overflow, ad esempio resettando il flag
feclearexcept(FE_OVERFLOW);
} else {
printf("Risultato: %e\n", risultato);
}
// Cambia la modalità di arrotondamento a round toward zero
fesetround(FE_TOWARDZERO);
// Esegui un'altra operazione in virgola mobile
double a = 5.7;
double b = 2.3;
double risultato2 = a / b; // Risultato approssimato
printf("Risultato con arrotondamento verso zero: %f\n", risultato2);
return 0;
}
In questo esempio, abilitiamo l'accesso all'ambiente di esecuzione matematica usando la direttiva #pragma FENV_ACCESS ON. Eseguiamo un'operazione in virgola mobile che potrebbe causare un overflow e verifichiamo se si è verificato usando la funzione fetestexcept(). Se l'overflow è stato rilevato, lo gestiamo resettando il flag corrispondente.
Successivamente, cambiamo la modalità di arrotondamento a "round toward zero" usando la funzione fesetround() e eseguiamo un'altra operazione in virgola mobile, stampando il risultato arrotondato secondo la nuova modalità.
Tutte queste operazioni, specialmente quelle che coinvolgono le modalità di arrotondamento, sono funzionalità molto specialistiche che vengono adoperate principalmente in ambito scientifico e ingegneristico, dove la precisione numerica è cruciale. Nella maggior parte delle applicazioni quotidiane, l'accesso all'ambiente di esecuzione matematica non è necessario.