According to IEEE-754 standard, every floating-point datum belongs to one of two disjoint classes:
- Numbers and
- Errors.
Numbers are floating-point numbers in mathematical sense. They can be used as arguments in arithmetic operations and function calls. For this class, constant folding is a natural and applicable optimization.
In contrast, the Errors, named in the Standard as “not-a-number” (NaN), are created when a floating-point operation is invalid and no meaningful numeric result exists. A special kind of NaN, the signaling NaN (SNaN) is often used to mark uninitialized values. Since NaNs are essentially error codes, arithmetic operations and mathematical function calls are meaningless for them, these operations just propagates the error from operand to the result.
Constant folding of the expressions that have NaN as operand or result can be meaningless or even unsafe for the following reasons:
First, NaNs represent runtime errors. Compiler can deduce that an operation is invalid and will produce a NaN. In most cases this indicates presence of an error that the frontend did not detect, but which was revealed in low level, for example, due to LTO optimizations. Probably the right behavior would be emitting a warning, but al low level it may be difficult to report where the error arises.
Replacing an instruction that performs an invalid operation with its expected value does not improve performance, as it is likely optimizing invalid code. A user may use the invalid operation intentionally to get NaN. In this case, constant folding is undesirable because the resulting NaN may depend on the target hardware.
If an instruction that produces NaN is executed, it raises an invalid exception. This exception can be caught by a debugger or by the running software (even if the default FP environment is used). However if the instruction is evaluated at compile-time, the exception is not raised, and the produced NaN result silently propagates through subsequent calculations.
Therefore, constant folding in this case hides errors and may create security vulnerabilities.|
Second, the exact representation of a NaN is target-dependent. Some targets support SNaN, while others do not. Canonical NaNs can be different for different platforms. Rules of payload propagation also differ. This variability creates many problems due to mismatch between actual and expected behavior, for instance:
- [ARM][AArch64] Vector intrinsics do not match hardware behavior for NaN, subnormals · Issue #128006 · llvm/llvm-project · GitHub ([ARM][AArch64] Vector intrinsics do not match hardware behavior for NaN, subnormals),
- Inconsistent run-time (on x86_64) and compile-time folding in NaN production · Issue #61973 · llvm/llvm-project · GitHub (Inconsistent run-time (on x86_64) and compile-time folding in NaN production),
- Wrong signs on division producing NaN · Issue #55131 · rust-lang/rust · GitHub (Wrong signs on division producing NaN)
This discrepancy has already been discussed, for instance, here: Semantics of NaN. The attempt to formalize NaN behavior in IR, as documented in LLVM Language Reference Manual — LLVM 22.0.0git documentation does not appear entirely successful, because the difference attributed to hardware anyway remains, as confessed in that document.
Denying constant folding instructions that produce or propagate NaN defers questions about NaN format and behavior to the hardware. That make the IR design more consistent and also make the produced code safer because potential error sources are not hidden.
The possible implementation is provided in: [ConstantFolding] Stop folding NaNs by spavloff · Pull Request #167475 · llvm/llvm-project · GitHub.