RFC: Calling functions if pragma FENV_ROUND is present

The implementation of pragma FENV_ROUND requires some support by the library. This RFC presents a possible way, how this support could be implemented.

Background

The latest C2x C standard draft introduces a pragma FENV_ROUND (https://www.iso-9899.info/n3047.html#7.6.2), which defines constant rounding mode in addition to dynamic, which is set by fesetround. These modes have natural representation on platforms that support static rounding, like RISC-V: if the mode is taken from an instruction field, it is constant mode, if from a register, it is dynamic. On other platforms, constant rounding can be implemented using dynamic rounding (7.6.2p5).

The Standard requires that the functions called in the scope of pragma FENV_ROUND be evaluated using dynamic rounding mode with some exceptions (7.6.2p4):

Within the scope of a FENV_ROUND pragma establishing a mode other than FE_DYNAMIC … invocations of functions indicated in the table below, for which macro replacement has not been suppressed (7.1.4), shall be evaluated according to the specified constant rounding mode (as though no constant mode was specified and the corresponding dynamic rounding mode had been established by a call to fesetround). Invocations of functions for which macro replacement has been suppressed and invocations of functions other than those indicated in the table below shall not be affected by constant rounding modes – they are affected by (and affect) only the dynamic mode.

That is, the same function may be evaluated using different rounding modes in the scope of pragma FENV_ROUND:

#pragma STDC FENV_ROUND FE_TOWARDZERO
y = sin(x);   // sin is evaluated using rounding to zero
z = (sin)(x); // sin is evaluated using dynamic rounding

The problem is how to implement such function calls.

Solution

The compiler could recognize if a called function requires constant rounding mode and replace it with some other code, for example with a call to a different function, like sin → sin_rtz. This way however looks inflexible. The compiler must do this transformation for all the functions mentioned in the Standard, no matter, if they are implemented or not. The library must know how the compiler transforms the functions and provide the necessary functions or macros for constant modes (like sin_rtz). Implementations of the compiler and the library become tightly coupled. In particular, if the set of these functions is extended (by using a new floating type, for example), the compiler would require changes.

The wording used in the Standard (“for which macro replacement has not been suppressed”) may be understood as a hint at using a preprocessor for this feature. The library could define macros for functions like sin to get implementation conformant to the Standard. For example, sin for FE_TOWARDZERO could be transformed to sin_rtz, which could be implemented as follows:

static inline double sin_rtz(double x) {
  int saved_mode = fegetround();
  fesetround(FE_TOWARDZERO);
  double res = (sin)(x);
  fesetround(saved_mode);
  return res;
}

Such transformation may be performed using a macro that represents the constant rounding mode set by pragma FENV_ROUND.

Proposal

Let’s introduce a macro __ROUNDING_MODE__, which expands to an identifier that reflects constant rounding mode For example, OpenCL modifiers (The OpenCL™ C Specification) may be used with addition of a value for FE_TONEARESTFROMZERO:

FE_TOWARDZERO         _rtz
FE_TONEAREST          _rte
FE_UPWARD             _rtp
FE_DOWNWARD           _rtn
FE_TONEARESTFROMZERO  _rta
FE_DYNAMIC	          empty

Using such macro, the special treatment of the functions mentioned in the Standard can be implemented with the simple macros:

#define CONCAT(a, b) CONCAT_(a, b)
#define CONCAT_(a, b) a##b
#define ADD_ROUNDING_MODE_SUFFIX(func) CONCAT(func, __ROUNDING_MODE__)
#define sin(x) ADD_ROUNDING_MODE_SUFFIX(sin)(x)

In this case the call of sin in the code:

#pragma STDC FENV_ROUND FE_TOWARDZERO
y = sin(x);

would be expanded to y = sin_rtz(x), which could have the implementation as inline function presented above or be implemented as a library function or be an another macro etc.

In this case the library provides the set of macro definition for the supported functions. The implementation may be any, the library support the only restriction is using values of __ROUNDING_MODE__ as a part of implementing functions or macros.

Any feedback is appreciated.

The compiler could recognize if a called function requires constant
rounding mode and replace it with some other code, for example with a
call to a different function, like |sin| → |sin_rtz|. This way however
looks inflexible. The compiler must do this transformation for all the
functions mentioned in the Standard, no matter, if they are
implemented or not. The library must know how the compiler transforms
the functions and provide the necessary functions or macros for
constant modes (like |sin_rtz|). Implementations of the compiler and
the library become tightly coupled. In particular, if the set of these
functions is extended (by using a new floating type, for example), the
compiler would require changes.
To some degree, this coupling is kind of inevitable. The set of
functions we’re talking about here are primarily those that are tending
towards becoming compiler intrinsics. The main exception to this set of
functions are those that are used to implement float<->string conversions.

The wording used in the Standard (/“for which macro replacement has
not been suppressed”/) may be understood as a hint at using a
preprocessor for this feature.

I don’t have a good history of the wording in the standard (I could
probably dig it up), but my first take on that phraseology is that it’s
intended to mean “you don’t have to play guessing games when called via
a function pointer” (admittedly, I’m not an expert in this level of C
language lawyering).

In this case the library provides the set of macro definition for the
supported functions. The implementation may be any, the library
support the only restriction is using values of |ROUNDING_MODE| as
a part of implementing functions or macros.

Two points of feedback here:

First, what are the actual C library implementations planning to do to
support this feature? This proposal presupposes that the libraries will
provide versions of sin_rtz et al, which, if they’re not going to go
in that direction, doesn’t provide a lot of help. I think coordination
with the library implementations is more useful than any feedback you’re
likely to get on this forum directly.

Second, the implementation approach I’ve thought about in my head is
some sort of attribute to indicate that “this method inherits the static
rounding mode when called directly”. Partially, this approach works
better in cases where functions don’t have names that work well with
operand pasting (say a C++ operator +, not that I think C++ is likely
to adopt this approach given past SG6 discussions). But also partially,
this means we don’t generate two extra pairs of rounding mode changes.

In any case, I think the answer is primarily dependent on how the C
library people want to move forward.

I could not find anything about history or discussion on this topic. The authors of this feature must have some ideas how it could be used and implemented. It would be useful to know these intentions.

As a first implementation we could have a file math.h as a compiler header, which could contain necessary declarations like:

#include_next <math.h>

static inline double sin_rtz(double x) {
  int saved_mode = fegetround();
  fesetround(FE_TOWARDZERO);
  double res = (sin)(x);
  fesetround(saved_mode);
  return res;
}

In this way we can get a standard-conformant implementation of the pragma without need to coordinate with the development outside LLVM. Probably it is the right way, as instead of fegetround and fesetround we could use __builtin_flt_rounds and __builtin_set_flt_rounds, functions like sin also could be replaced with __builtin_sin. It could make the implementation better, but the practical solution may be too compiler-dependent.

An attribute you describes makes sense and it could help implementation of the pragma, but it alone is not sufficient. For example, if a target supports static rounding, it may be preferable to have different functions for different rounding modes. In this case the compiler must generate different calls for sin depending on the static rounding mode. Also the requirement to use dynamic mode if macro expression is suppressed must be implemented in the compiler. This attribute is a separate feature, dependent on the pragma.

I do believe the intent here was that the C library’s math.h header would have macro definitions, like you suggest.

In my opinion, this ought be implemented as a feature of the platform’s libc, not as something Clang does in a math.h overlay. In that case, the only things Clang needs to do is:

  1. Expose the expected preprocessor symbol.
  2. (optionally) Recognize the libc’s new ABI as known libcalls for optimization purposes.

There are a few things to keep in mind:

  1. C++ has made the entire math library constexpr and the recommended practice in C++ is to match runtime and compile time evaluation behavior whenever possible. If this pragma is exposed in C++, we may want this to be implemented in the compiler rather than the system libc.

  2. WG14 has been beefing up their constant expressions (C23 added constexpr objects, but not functions; there’s some sentiment within the committee to add constexpr functions for C2y+), and C has the same recommendation as C++ regarding constant evaluation. So we may want this in the compiler even for C.

If static rounding modes are adopted for C++, I’d expect it to be done via something like:

// Returns the pragma STDC FENV_ROUND mode in the current scope.
// When used as a default argument, uses the value for the callers' scope,
// (similar to the behavior of std::source_location).
consteval rounding_mode current_static_rounding_mode();
double cos(double val, rounding_mode rnd = current_static_rounding_mode());

For future C constexpr: I would hope a constexpr C math library (if such a thing ever exists) would be implemented in the libc’s headers. Likely, the libc headers would explicitly delegate to the compiler via something like calling __builtin_cos_explicit(double val, int rounding_mode) – but explicitly, not magic in the compiler recognizing a function name.

I was speaking less about what WG21 does and more about how we typically expose conforming extensions across language boundaries.

I thought the way library builtins worked was that we would identify the builtin by name + header file, so the compiler would override the library’s definition in some cases. But I must be wrong about that because: Compiler Explorer

The key point here is, I think, what the libc implementations want the entry-point to look like: should it be separate for each combination of function/rounding-mode, should it take te rounding mode as an integer argument, or should the compiler just save/restore the rounding mode with fegetround/fesetround?

If we want separate entry points, exposing the rounding mode as an identifier might be convenient; if we don’t, exposing it as a number probably makes more sense.


If we want the compiler to do constant-folding, whatever scheme we use, the compiler needs to recognize the names. In that case, we actually can’t let the libc choose its own names: the compiler and the libc have to agree on the name.

But I must be wrong about that because: Compiler Explorer

We recognize strlen, we just refuse to constant-fold it in that context because the C++ standard says strlen isn’t constexpr. (Internally, __builtin_strlen is a separate name, which CodeGen lower to a call to strlen.)

1 Like

It is important to have a flexible solution so that each function could be implemented in its own way, depending on hardware capabilities, performance, precision, convenience and so on. For example, printf most likely would be implemented using dynamic rounding mode, even on a target with static rounding, but fadd on the same target could be implemented using static rounding. Some rounding modes may be unsupported on a target directly, but it could be desirable to have some functions using such mode. Approximation coefficients could depend on the rounding mode. Separate functions for each combination of function/rounding mode seems a more flexible solution. Also passing rounding mode as a function argument can make negative impact on the code performance and size as the rounding mode becomes a runtime value.

Making compiler save/restore the rounding mode with fegetround/fesetround is attractive functionality as many targets do not support static rounding mode. It however is not suitable for all cases. To keep flexibility this mode switching could be made by compiler builtins like __builtin_cos(double val) or __builtin_cos_explicit(double val, int rounding_mode).

To have a solution usable in C also we could introduce a macro, that would represent the current static rounding mode, like __STATIC_ROUNDING_MODE__ or __STATIC_FLT_ROUNDS__.