RFC: Change of strict FP operation representation in IR

No problem. We can use the same encoding as we do now for constrained intrinsics, it is familiar to developers. Taking into account the note below about splitting “fp.control” and making similar changes in denorm bundles, the syntax could be:

call float @llvm.nearbyint.f32(float %x) [ "fp.round"(metadata !"tonearest"), "fp.denorm.in"(metadata !"preserve-sign"), "fp.denorm.out"(metadata !"positive_zero"), "fp.except"(metadata !"ignore") ]

It depends on how we want to model these operations in LLVM IR. if @fadd() ["fp.round"(metadata !"round_to_nearest")] does not have a side effect, it means that rounding mode in this case is considered as static. This representation is slightly more abstract than the existing constrained intrinsics. But it has advantages including:

  • more possibilities for optimizations,
  • natural implementation of pragma STDC FENV_ROUND,
  • possibility to use static rounding in default mode.

It however requires a dedicated pass that would insert necessary set_/get_rounding calls to preserve the semantics on targets without static rounding.

Using static rounding for specific (non-dynamic) modes seems like a good approach. It allows us to use static rounding freely in LLVM IR for any target, but eventually we should replace static rounding with dynamic before the instruction selection.

The key point is that exceptions cannot be ignored in strictfp environment, and fpexcept.ignore cannot change that. For example:

inline float divide(float x, float y) {
  // default fp mode.
  return x / y;
}

float calculate() {
  #pragma STDC FENV_ACCESS ON
  …
  y = sin(x);
  exc = fetestexcept(MASK))
  …
  c = divide(a, b);
  …
}

In this case, the divide function produces a division instruction with fpexcept.ignore flag, since it originates from a default-mode function. If this instruction had fp_status: none attribute, nothing would prevent the compiler from placing the instruction between the instruction that use exceptions:

  y = sin(x);
  c = divide(a, b);
  exc = fetestexcept(MASK))

In a strictfp function, an IR instruction fadd with fp_status: none cannot be realized with just a single target fadd instruction - it requires at least a pair of fegetenv and fesetenv. This makes the use of fp_status: none in strictfp functions a source of misunderstanding. Most transformation based on this property in LLVM seem to be incorrect. The only valid use is eliminating instructions with unused results, even if the instruction may raise exceptions. For example, the following call may be removed by compiler, although it raises an exception:

    %unused = call double @llvm.experimental.constrained.fdiv.f64(double 0.0, double 0.0, metadata !"round.upward", metadata !"fpexcept.ignore" 

Some users think this behavior is incorrect. If they are right, fp_status: none in strictfp function is useless, instructions with and without this attribute behave identically. Consequently the entire parameter “fpexcept” can be removed, as the exception handling would be determined only by the function attribute ‘strictfp’.

If we choose to maintain per-instruction exception handling flags, we must define its semantics more precisely and establish a set of potential use cases for this flags. I would drop it completely, - exception handling is defined by the entire code, it is not a choice of an individual function. It however could be made later, no need to tie this decision with this proposal.

The current implementation supports only two models: default mode and strictfp mode with dynamic rounding. It is possible to create an IR with instructions that use specific rounding modes, but clang does not produce such code and it is unlikely that some MLIR or other LLVM-based code generator creates strictfp code that uses non-dynamic rounding.

So the current functionality of constrained FP intrinsics consists of only dynamic rounding. All constrained intrinsics have side effect to prevent undesirable reorder and this is vital for strictfp implementation. All these properties are preserved in the proposed implementation.

I would propose to consider difference between static and assumed modes as a low-level implementation detail and leave the choice between them to the compiler. fadd executes identically (produces the same results) no matter if rounding mode is specified in the instruction or is read from a register.

If a case arises where it is really necessary to distinguish between them, rounding mode bundle could be amended with an appropriate flag, for example:

call float @llvm.nearbyint.f32(float %x) [ "fp.round"(metadata !"tonearest,dynamic" ]

Yes, you are right. Support of static rounding mode requires some IR transformation that would insert set_rounding and get_rounding to emulate semantics of static rounding. However it is required anyway if we want to implement pragma FENV_ROUND, this is not a drawback of this proposal. The C standard explicitly points out that static rounding may be implemented in this way: https://www.iso-9899.info/n3047.html#7.6.2p5. And yes, a target with hardware support of static rounding would have benefits.

Most of the information needed for the transformation, that converts static rounding to dynamic, can be obtained from analyzing the function code. If pragma FENV_ROUND is used without FENV_ACCESS, we know that no user function modifies rounding mode, and we can insert functions that change rounding mode optimally. Otherwise the user intends to mix static and dynamic modes and the things are more complicated. Compiler intrinsics and standard C functions have known behavior and the pass can use this information. The main challenge lies with user functions, they can change rounding mode and should be treated as barriers making rounding mode unknown. It this is not sufficient, a special function attribute indicating that the function changes rounding mode, can be introduced.