Background
Floating-point operations are unique in that they have different representations depending on the use case. In the default mode, FP operations are represented by instructions such as fadd
or intrinsic functions like llvm.trunc
, which appear to be pure functions. It is not true in the general case, as on many cores FP operations may depend on the contents of some hardware register that stores options like rounding mode and some other. Additionally, these operations can change bits in some register to report events that occur during the evaluation. In strict FP mode these interactions are not ignored and are represented as a side effects associated with the operation. Consequently, in this mode FP operations must be represented differently to reflect these side effects.
In the current LLVM implementation, FP operations in strict FP mode are represented by constrained intrinsic function calls, such as llvm.experimental.constrained.fadd
or llvm.experimental.constrained.trunc
. These functions always have side effects. They also have additional arguments, which are compiler hints.
The problem
While the current solution works well for typical cases, it faces challenges in more complex scenarios (Thought on strictfp support). In particular:
- It requires two separate functions for each FP operation even though these functions are processed almost identically,
- It is difficult to extend this solution to target intrinsics,
- It cannot accommodate other FP options, such as the treatment of denormals,
- It is hard to support FP control modes specified directly in instructions, such as static rounding (Static rounding mode in IR).
Proposal
The alternative solution is based on these assumptions:
- Some intrinsics functions (floating-point operations) may have side effects depending on whether the containing function has the
strictfp
attribute. In the functions that use the default FP mode, these intrinsics behave as pure functions, if strict FP mode is required, they may have side effects. - Calls to these intrinsics may have special operand bundles, which may be, in particular, compiler hints, or may specify options for the operation, like rounding mode.
The use of operand bundles to pass compiler hints is not a new idea. There was an attempt to replace constrained intrinsics with operand bundles (â D93455 Constrained fp OpBundles), but this effort was not completed.
Two kinds of FP operand bundles are defined, which generally reflect the arguments of existing constrained functions:
- Effective control mode set. Now it only includes the rounding mode. It is the rounding mode used for evaluating the called function result. It may be a mode specified in the control register, if compiler can deduce it. It also can be static rounding mode, stored in the corresponding instruction. This kind of bundle is identified by the tag fp.control. In future it may be extended to include other control modes.
- Exception handling. This bundle has exactly the same meaning as the corresponding argument in constrained function call. It is identified by the tag fp.except.
The operand bundles are represented as metadata:
call float @llvm.nearbyint.f32(float %x) [ "fp.control"(metadata !"dyn"), "fp.except"(metadata !"strict") ]
Rounding mode is specified by one of the strings: âdynâ, ârteâ, ârtzâ, ârtpâ, ârtnâ, ârmmâ. Exception handling can take one of the values: âignoreâ, âmaytrapâ and âstrictâ.
Operand bundles are optional. Unspecified parameters are assumed to have default values, which depends on the attributes of the enclosing function . For example, the following calls are equivalent in strict FP mode:
call float @llvm.nearbyint.f32(float %x) [ "fp.control"(metadata !"dyn"), "fp.except"(metadata !"strict") ]
call float @llvm.nearbyint.f32(float %x) [ "fp.except"(metadata !"strict") ]
call float @llvm.nearbyint.f32(float %x) [ "fp.control"(metadata !"dyn") ]
call float @llvm.nearbyint.f32(float %x)
And the following are identical in default FP mode:
call float @llvm.nearbyint.f32(float %x) [ "fp.control"(metadata !"rte"), "fp.except"(metadata !"ignore") ]
call float @llvm.nearbyint.f32(float %x) [ "fp.except"(metadata !"ignore") ]
call float @llvm.nearbyint.f32(float %x) [ "fp.control"(metadata !"rte") ]
call float @llvm.nearbyint.f32(float %x)
Advantages
The proposed solution has several advantages:
- The same function call, such as
call float @llvm.trunc.f32(float %x)
may be used for both default and strict modes. No more need of separate functions for strict and default modes. - Any intrinsic function can get support in the strict mode with minimal changes.
- Hints can be specified in any mode. In particular it allows using static rounding in default mode.
- The set of supported hints and options can be easily expanded.
Implementation
There are the MRs, that try to implement this solution:
Implement operand bundles for floating-point operations by spavloff ¡ Pull Request #109798 ¡ llvm/llvm-project ¡ GitHub, in which the operand bundles are introduced and exist together with the constrained functions.
Reimplement constrained 'trunc' using operand bundles by spavloff ¡ Pull Request #118253 ¡ llvm/llvm-project ¡ GitHub, which is an example how an intrinsic can be modified to use the new solution only.
The implementation plan looks as follows:
- Implement operand bundles, the !109798 does this,
- Update intrinsics to use the operand bundles. It can be made gradually. After this step intrinsic functions use the new mechanism and only constrained counterparts of instructions, like
fadd
remain. - Introduce new intrinsics like
llvm.fadd
and use them in strictfp functions. After this step the transition is complete.
Any feedback is appreciated.