The Mathematical Execution Environment in C
The IEEE 754 standard represents the most widespread format for representing and manipulating floating-point numbers. This standard is also known as IEC 60559.
Although the C language and its standard mathematical library abstract access to hardware and the functionalities of processors that use it, it may happen that some applications (especially those that make heavy use of numerical calculations) need to access low-level functionalities directly. These functionalities collectively take the name of Mathematical Execution Environment or, in English, Floating Point Environment.
To access the environment, the C standard library provides a header file called <fenv.h>. In this lesson we will see how to use and manage the execution environment.
- The mathematical execution environment controls the behavior of floating-point operations.
- It is composed of floating-point status flags and control modes.
- The C standard library provides functionalities to access and manage the mathematical execution environment.
- These functionalities are declared in the
<fenv.h>header. - These are advanced functionalities that require explicit enabling of access to the mathematical execution environment and are mainly used in high-performance numerical applications.
Floating Point Status Flags and Control Modes
A Floating Point Status Flag is a system variable that is set to true when a floating-point error occurs. Often, when the underlying processor implements floating-point operations in hardware, it is an actual processor register.
In the IEEE 754 standard, five types of floating-point errors are defined:
- overflow;
- underflow;
- division by zero;
- invalid operation, when the result of an operation is NaN (Not a Number);
- inexact result, when the result of an operation has been approximated.
Each of these errors corresponds to a status flag that can be true or false.
The <fenv.h> header of the standard library defines a particular type, fexcept_t, that can be used to work with the status flags of the environment. An object of type fexcept_t represents the collective value of all floating-point status flags. Although it may be an integer in which each bit represents a flag, its implementation is left to the compiler and platform according to the C standard.
Another important concept in the mathematical execution environment is that of control modes (floating point control modes). A control mode is a system variable that determines the future behavior of floating-point operations.
A common control mode is the rounding mode, which determines how the results of floating-point operations are rounded when they cannot be represented exactly. The IEEE 754 standard defines four rounding modes:
- Round toward nearest: rounds to the nearest number. In case of a tie, rounds to the number whose least significant bit is zero (round half to even).
- Round toward zero: rounds toward zero (truncation).
- Round toward positive infinity: rounds toward positive infinity (ceiling).
- Round toward negative infinity: rounds toward negative infinity (floor).
By default, the IEEE 754 standard specifies that the rounding mode must be "round toward nearest".
Some implementations, including hardware ones, can support other control modes:
- Rounding Precision: determines the precision with which floating-point results are rounded (for example, 24 bits for
float, 53 bits fordouble, etc.). - Error Handling: determines how the system responds when a floating-point error occurs (for example, by generating an exception, ignoring the error, etc.).
Mathematical Execution Environment
The Mathematical Execution Environment or Floating Point Environment is the union of floating-point status flags and control modes. It represents the current state of floating-point operations and can be manipulated through the functions defined in the <fenv.h> header of the C standard library.
To represent the mathematical execution environment, the <fenv.h> header defines a type called fenv_t. An object of type fenv_t represents the complete state of the mathematical execution environment, including all status flags and control modes.
To manipulate the mathematical execution environment, the <fenv.h> header defines some macros:
| Macro Name | Value | Description |
|---|---|---|
FE_DIVBYZERO |
Integer constant | Represents the status flag for division by zero. |
FE_INEXACT |
Integer constant | Represents the status flag for inexact result. |
FE_INVALID |
Integer constant | Represents the status flag for invalid operation. |
FE_OVERFLOW |
Integer constant | Represents the status flag for overflow. |
FE_UNDERFLOW |
Integer constant | Represents the status flag for underflow. |
FE_ALL_EXCEPT |
Integer constant | Represents all floating-point status flags. |
| Macro Name | Value | Description |
|---|---|---|
FE_TONEAREST |
Integer constant | Represents the "round toward nearest" rounding mode. |
FE_DOWNWARD |
Integer constant | Represents the "round toward negative infinity" rounding mode. |
FE_UPWARD |
Integer constant | Represents the "round toward positive infinity" rounding mode. |
FE_TOWARDZERO |
Integer constant | Represents the "round toward zero" rounding mode. |
The macros that represent status flags have distinct values that can be combined using the bitwise OR operator (|). For example, FE_DIVBYZERO | FE_OVERFLOW represents both the status flags for division by zero and overflow.
The FE_ALL_EXCEPT macro represents the bitwise OR of all defined status flags.
According to the C standard, it is not guaranteed that all the macros defined above are present in the implementation. To verify which macros are supported, one must check the value of FE_ALL_EXCEPT. If a macro is not supported, its corresponding bit in FE_ALL_EXCEPT will be zero. If the macro equals zero, it means that no status flag macro is supported.
The macros that represent rounding modes are mutually exclusive: only one of them can be active at a given time. The current rounding mode can be obtained using the fegetround() function, while it can be modified using the fesetround() function. We will see later how to use these functions.
An important macro defined in the <fenv.h> header is FE_DFL_ENV, which represents the default mathematical execution environment. When we talk about default, we mean the initial state of the mathematical execution environment when a C program begins its execution. In fact, every C program has its own mathematical execution environment, which is initialized to the value of FE_DFL_ENV at the beginning of the program.
Enabling Access to the Mathematical Execution Environment
Normally, access to the mathematical execution environment is disabled for performance reasons.
To access it, the <fenv.h> header defines a #pragma directive called FENV_ACCESS that can be used to signal to the compiler that the program needs to access the mathematical execution environment.
To use it, the syntax is as follows:
#include <fenv.h>
#pragma STDC FENV_ACCESS value
where value can be ON, OFF or DEFAULT.
When value is ON, access to the mathematical execution environment is enabled. When value is OFF, access is disabled. When value is DEFAULT, the default compiler behavior is restored, which may enable or disable access depending on the implementation.
The duration of the effect of the #pragma FENV_ACCESS directive depends on the point where it is inserted in the source code. It can only be inserted in three places:
-
In a source file, globally outside any function.
In this case, the directive takes effect for the entire source file starting from its position. Or until another
#pragma FENV_ACCESSdirective is encountered that modifies its value. In any case, until the end of the source file in which it was inserted. -
At the beginning of a code block.
In this case, the directive takes effect for the entire code block in which it was inserted. Or until another
#pragma FENV_ACCESSdirective is encountered that modifies its value. In any case, until the end of the code block in which it was inserted.For example, it can be inserted at the beginning of the body of a function to enable or disable access to the mathematical execution environment for the entire duration of the function:
#include <fenv.h> double example_function(double x) { #pragma STDC FENV_ACCESS ON // Function body with access // to the mathematical execution environment enabled // ... } double another_function(double y) { #pragma STDC FENV_ACCESS OFF // Function body with access // to the mathematical execution environment disabled // ... } double default_function(double z) { // Function body with access // to the mathematical execution environment // according to the default compiler behavior // ... }At the end of the code block, access to the mathematical execution environment returns to the value it had before the
#pragma FENV_ACCESSdirective was set.
Obviously, it is the programmer's responsibility to ensure that access to the mathematical execution environment is enabled when using functions that manipulate it. If access is disabled, the behavior of such functions is undefined according to the C standard.
The typical use of the #pragma FENV_ACCESS directive is to enable it at the beginning of a function that needs to access the mathematical execution environment, and disable it at the end of the function. In this way, the impact on program performance is minimized, limiting access only to the parts of code that actually need it.
#include <fenv.h>
double calculate_value(double x) {
#pragma STDC FENV_ACCESS ON
// Function body that accesses
// the mathematical execution environment
// ...
}
The calculate_value function has access enabled, so it can check for the presence of floating-point errors and modify rounding modes as needed.
Another important detail is that when transitioning from a code region with OFF access to one with ON access, the state of the flags is indeterminate until a floating-point operation is executed.
Functions for Managing Floating-Point Errors
The <fenv.h> header of the C standard library provides five functions for managing floating-point status flags. Each of these functions takes as input a mask of status flags, which can be obtained by combining the macros defined earlier using the bitwise OR operator (|).
-
int feclearexcept(int excepts);This function resets (sets to false) the status flags specified by the
exceptsmask. Returns0on success, or a non-zero value on error. -
int feraiseexcept(int excepts);This function sets (to true) the status flags specified by the
exceptsmask. Returns0on success, or a non-zero value on error. -
int fetestexcept(int excepts);This function checks the state of the status flags specified by the
exceptsmask. Returns a bit mask in which each bit corresponding to a status flag specified inexceptsis set to1if the flag is true, or to0if the flag is false.For example, to check if an overflow or underflow has occurred, one can use the following code:
#include <fenv.h> // Enable access to the mathematical execution environment #pragma STDC FENV_ACCESS ON // Execute a floating-point operation // that could cause an overflow or underflow double result = floating_point_operation(); // Check if an overflow occurred if (fetestexcept(FE_OVERFLOW)) { // Handle the overflow } else if (fetestexcept(FE_UNDERFLOW)) { // Handle the underflow } -
int fegetexceptflag(fexcept_t *flagp, int excepts);This function saves the state of the status flags specified by the
exceptsmask in theflagpobject of typefexcept_t. Returns0on success, or a non-zero value on error. -
int fesetexceptflag(const fexcept_t *flagp, int excepts);This function restores the state of the status flags specified by the
exceptsmask using theflagpobject of typefexcept_t. Returns0on success, or a non-zero value on error.The
flagpobject must have been previously obtained using thefegetexceptflag()function.
Functions for Managing Rounding Modes
The <fenv.h> header of the C standard library defines two functions for managing floating-point rounding modes:
-
int fegetround(void);This function returns the current rounding mode as one of the macros defined earlier (
FE_TONEAREST,FE_DOWNWARD,FE_UPWARD,FE_TOWARDZERO). If an error occurs, it returns a value different from these macros. -
int fesetround(int round);This function sets the current rounding mode using the
roundmacro, which must be one of the macros defined earlier (FE_TONEAREST,FE_DOWNWARD,FE_UPWARD,FE_TOWARDZERO). Returns0on success, or a non-zero value on error.
In addition to these two functions, the <float.h> header of the C standard library defines a macro called FLT_ROUNDS that represents the current rounding mode as an integer:
| Value | Rounding Mode |
|---|---|
| -1 | Indeterminate |
| 0 | Round toward zero |
| 1 | Round to nearest |
| 2 | Round toward positive infinity |
| 3 | Round toward negative infinity |
Functions for Managing the Mathematical Execution Environment
The <fenv.h> header of the C standard library defines four functions for managing the mathematical execution environment:
-
int fegetenv(fenv_t *envp);This function saves the current mathematical execution environment in the
envpobject of typefenv_t. Returns0on success, or a non-zero value on error. -
int fesetenv(const fenv_t *envp);This function restores the mathematical execution environment using the
envpobject of typefenv_t. Returns0on success, or a non-zero value on error.The
envpobject must have been previously obtained using thefegetenv()function or can beFE_DFL_ENVto restore the default mathematical execution environment. -
int feholdexcept(fenv_t *envp);This function performs three operations:
- Saves the current mathematical execution environment in the
envpobject of typefenv_t. - Resets all floating-point status flags.
- Sets the control mode to non-stop (that is, floating-point operations do not generate exceptions).
Returns
0on success, or a non-zero value on error. - Saves the current mathematical execution environment in the
-
int feupdateenv(const fenv_t *envp);This function performs three operations:
- Saves the current floating-point status flags in a temporary variable.
- Restores the mathematical execution environment using the
envpobject of typefenv_t. - Raises exceptions for all status flags that were true in the temporary variable.
Returns
0on success, or a non-zero value on error.
Example of Using the Mathematical Execution Environment
Here is an example of using the mathematical execution environment in C:
#include <stdio.h>
#include <fenv.h>
#include <math.h>
#pragma STDC FENV_ACCESS ON
int main() {
// Execute a floating-point operation
double x = 1.0e308;
double y = 1.0e10;
double result = x * y; // Could cause overflow
// Check if an overflow occurred
if (fetestexcept(FE_OVERFLOW)) {
printf("Overflow detected!\n");
// Handle the overflow, for example by resetting the flag
feclearexcept(FE_OVERFLOW);
} else {
printf("Result: %e\n", result);
}
// Change the rounding mode to round toward zero
fesetround(FE_TOWARDZERO);
// Execute another floating-point operation
double a = 5.7;
double b = 2.3;
double result2 = a / b; // Approximated result
printf("Result with rounding toward zero: %f\n", result2);
return 0;
}
In this example, we enable access to the mathematical execution environment using the #pragma FENV_ACCESS ON directive. We execute a floating-point operation that could cause an overflow and check if it occurred using the fetestexcept() function. If the overflow was detected, we handle it by resetting the corresponding flag.
Subsequently, we change the rounding mode to "round toward zero" using the fesetround() function and execute another floating-point operation, printing the result rounded according to the new mode.
All these operations, especially those involving rounding modes, are very specialized functionalities that are mainly used in scientific and engineering fields, where numerical precision is crucial. In most everyday applications, access to the mathematical execution environment is not necessary.