What guarantees are there about the behavior of floating-point code?
Almost none.
At most you can use ADD, SUB, MUL, DIV, and SQRT on operands that are not infinities, not negative zero, and not denormal, using round-nearest-even, and expect a reasonable result at 0.5 ULP. Otherwise, you are in ID behavior, even for architectures who do implement sound IEEE 754 compliant feature functionality.
You can count on none of the comparisons (x < y) to have the properties specified in IEEE 754, and none of the recommended functions, including things as easy as COPYSIGN(). You cannot count on accessing the rounding modes, you cannot assume changing rounding modes work, or that changing rounding mode is inexpensive. Many times an implementation will have implemented something that is frustratingly close to what IEEE 754 specifies, but fails at some insundry boundary condition. example::
if( x < y )
then-clause
else
else-clause
IEEE specifies that if x or y is NaN then control is transferred to the else-clause. There are compilers which actively disobey this subtly, rearranging then and else clauses willy nilly.
You SHOULD NOT try to optimize anything across a parenthesis boundary (FORTRAN mandates this, and uses parens to force order of evaluation. Here, FP is simply not like integer, and the good programmer has gone out of his/her way to structure the code such that is it evaluated in the order specified in ASCII source code. I have seen cases where adding 0.5 twice has more accuracy than adding 1.0 once–luckily IEEE 754 eliminated this horseplay.)
You cannot trust the accuracy of any IEEE 754 recommended {transcendental} function with an argument that “needs argument reduction” or touches anywhere near to a range or domain condition of the algorithm {tan(π/4)}. So, in general you cant trust anything–and here–the compiler BETTER NOT be trying to optimize ANYTHING. {Unless, as described below, you are a budding numerical analysist.}
If IEEE 754 is the intended model, then x87 codegen is completely broken and probably other targets are as well.
There are many who take the position that x87 is not compliant with IEEE 754, and it is certainly not compliant with IEEE 754-2008 or 2019. Then there who insist that it does comply.
Even if x87 were completely compliant with IEEE 754-2019, you would not want programmers who actually understand numerical analysis to have their subroutine compiled for x87 instead of SSE. The numerical properties are significantly different–leading to astonishing problems when assuming one and getting the other; both directions.
If not IEEE 754, then what is the intended model?
Even within IEEE 754 compliant architectures, you have implementations that Flush Denorms to Zero, implement only Round to Nearest, and fail to provide access to 754 specified functions {mainly in the comparisons and transcendental areas of the standard}. Heck, many architectures do not even bother to give the compiler instructions which transfer control properly to the else-clause on detection of a NaN operand (above example).
If you want to optimize floating point arithmetic, you have to stick to ADD, SUB, MUL, DIV, and SQRT; using only RNE or else understand the numeric properties of the target implementation such that you can write numeric proofs of transcendental functions using the primitives of the target instruction set.
About the only other thing that is safe is reading/writing ASCII FP numbers and to/from binary bit patterns–but here you still have to stay away from NaNs, Infinities, Denorms, and negative zero unless you are the aforementioned budding numerical analysist.
A long time ago there was a large body of numerical analysists that were employed as programmers and as verifiers who’s job it is to check and certify that one algorithm or another produces the “right” result for all arguments in the defined range. With IBM, CDC, and CRAY-like arithmetics (read poor FP numerics) These people lived comfortable in the knowledge their jobs were secure.
Then IEEE 754-1984 came along and programmers began to think that anyone can write good numeric codes, and a large part of what numerical analysts did has been lost. Later (mid-90s) compilers started getting good enough to start significant FP optimizations, proceeding until this day. Compiler writers should, at least, not try to make the situation worse by making whatever is left of the NA practice by optimizing things that should not be optimized or by rearranging instructions which can change results of the algorithm in subtle and annoying ways.
You might get the hint that this is a tricky area::
I have spend the last 40 years as a computer architect, spending the 8 years prior writing compilers.
I work with a LLVM compiler guy (Brian) and sometimes he comes up with a potential FP optimization, or I see that some optimization has been performed; and I have to go back through IEE 754-2019 to find the actual specification, reason about the standard, reason about the arithmetic properties, the optimization and the implementation before making very subtle alterations to my floating point unit specification on the target implementation before I sign off on the optimization. {So, the optimization does not interfere with the robustness of the final product.}
But I want to end up with a solidly-robust -2019 compliant floating point of my ISA (surprise coefficient as close to 0 as possible). And you only get 1 chance to get the architectural definition correct so that it survives the lifetime of the architecture. Otherwise you get into the bug-for-bug compatible morass that x87 (and others) finds itself.
Given that the architecture {like x86-64} is already fixed in stone, the opportunities are small indeed, and the dangers loom large, and the subtly is very hard to discover and analyze.