| > A decent programming language should have 2 distinct types for floating-point numbers, one for those that cannot be NaNs (to be used only in a program that enables and handles invalid operation exceptions) and one for floating-point numbers that may be NaNs (to be used in programs that disable the invalid operation exception, like most programming languages wrongly do by default). You've already explained why such a type separation is a bad idea - those types being only usable with specific setup; so, completely utterly breaking composability. Never mind this making float ops impure & stateful (also forbidding autovectorizing anything with more than one potentially-NaN-producing op if you don't want to break semantics). Perhaps if hardware supported embedding exception behavior in individual instructions this'd not be insane, but I haven't heard of any architecture having such, making it a complete non-option for sane languages designed to be used. (there is the option of making compilers insert the necessary fp state transitions, though then you'll have the desire to embed it into calling conventions & function types to avoid transitions when a certain state is expected to stay for a prolonged period of time, which, while certainly possible and would be quite neat to have (also for rounding modes, FTZ/DAZ, etc), is very much in the territory of something almost noone will bother doing, and as such things would just quietly be slower) While partially-ordered types are neat, what can you really sanely do with a comparison over such? Seems like a rather pointless thing to have. |
They depend on a set of configuration flags, which are stored in some internal register, e.g. the MXCSR register of Intel/AMD CPUs.
The same program can provide wildly different results if you run it twice, with different settings in the MXCSR register.
Any programmer who writes floating-point computations should be very aware that any FP operation depends on the state of a global register.
Hopefully, all functions that are invoked are well behaved and if they ever change the global settings they restore them to the value that the register had upon entry, otherwise the effects would be unpredictable.
What you say about the floating-point operations being "impure" is incorrect. A pure function may have not only explicit input parameters, but also implicit input parameters, a.k.a. global variables that are read-only.
For example any such function can be marked with __attribute__((pure)) in compilers like gcc and clang.
The functions that have only explicit input parameters are a subset of the pure functions. For example, such functions are marked with __attribute__((const)) in the gcc and clang compilers.
In a rigorous manner, FP operations would not belong in an __attribute__((const)) function, but the compilers do not care about the global variables that are CPU registers, because they assume that those are preserved by all functions (and upon context changes they are saved and restored by the operating system).
Enabling some or all kinds of FP exceptions has absolutely no influence on auto-vectorization. It also has no influence on speed, in well-designed CPUs.
The only difference is that the 1st error will crash the program, while with all errors masked the program will terminate successfully regardless how many errors happened. However in this case the user should analyze the results, to determine whether they are correct.
If with the exceptions masked, the programmer fails to test for NaNs wherever necessary, then the errors will be lost and bad results will be wrongly considered as correct.
Many real life things can be modeled correctly only as partially-ordered sets. When this is ignored and they are handled as totally-ordered sets, it is very easy to generate erroneous results. This can be avoided by various tests before comparing any values, but this is exactly what would be done automatically by the compiler in a programming language where data types that are partially ordered can be defined.
Having certain exceptions enabled only for an operation or for a function, instead of for an entire executable program, would be a very bad idea.
The reason is that this would offer no guarantees that can simplify the program. If you know that the invalid exception is enabled, then there cannot exist NaNs and you can omit testing for them everywhere. Similarly, if the overflow exception is enabled, you know that there cannot be infinities, so you can omit any tests for infinities.
If exceptions were enabled only for a part of the program, then the rest of the program can still generate special values and you need to test for them everywhere. So you get the worst of both choices, a complex program that can be crashed by an error.