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.

Key Takeaways
  • 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 for double, 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.
Table 1: Macros for 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.
Table 2: Macros for floating-point rounding modes.

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:

  1. 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_ACCESS directive is encountered that modifies its value. In any case, until the end of the source file in which it was inserted.

  2. 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_ACCESS directive 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_ACCESS directive 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 excepts mask. Returns 0 on success, or a non-zero value on error.

  • int feraiseexcept(int excepts);

    This function sets (to true) the status flags specified by the excepts mask. Returns 0 on success, or a non-zero value on error.

  • int fetestexcept(int excepts);

    This function checks the state of the status flags specified by the excepts mask. Returns a bit mask in which each bit corresponding to a status flag specified in excepts is set to 1 if the flag is true, or to 0 if 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 excepts mask in the flagp object of type fexcept_t. Returns 0 on 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 excepts mask using the flagp object of type fexcept_t. Returns 0 on success, or a non-zero value on error.

    The flagp object must have been previously obtained using the fegetexceptflag() 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 round macro, which must be one of the macros defined earlier (FE_TONEAREST, FE_DOWNWARD, FE_UPWARD, FE_TOWARDZERO). Returns 0 on 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
Table 3: FLT_ROUNDS macro for the current rounding mode.

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 envp object of type fenv_t. Returns 0 on success, or a non-zero value on error.

  • int fesetenv(const fenv_t *envp);

    This function restores the mathematical execution environment using the envp object of type fenv_t. Returns 0 on success, or a non-zero value on error.

    The envp object must have been previously obtained using the fegetenv() function or can be FE_DFL_ENV to restore the default mathematical execution environment.

  • int feholdexcept(fenv_t *envp);

    This function performs three operations:

    1. Saves the current mathematical execution environment in the envp object of type fenv_t.
    2. Resets all floating-point status flags.
    3. Sets the control mode to non-stop (that is, floating-point operations do not generate exceptions).

    Returns 0 on success, or a non-zero value on error.

  • int feupdateenv(const fenv_t *envp);

    This function performs three operations:

    1. Saves the current floating-point status flags in a temporary variable.
    2. Restores the mathematical execution environment using the envp object of type fenv_t.
    3. Raises exceptions for all status flags that were true in the temporary variable.

    Returns 0 on 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.