Hacker News new | ask | show | jobs
by dzaima 14 days ago
> 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.

1 comments

Floating-point operations are stateful in any CPU made during the last 40 years.

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.

> For example any such function can be marked with __attribute__((pure)) in compilers like gcc and clang.

https://gcc.gnu.org/onlinedocs/gcc/Common-Attributes.html states:

> However, functions declared with the pure attribute can safely [...] in a way that does not affect [...] or the observable state of the program

So an exception-aware FP op cannot be __attribute__((pure)) in a program that may read the FP status later, or has trapping on FP exceptions enabled.

This is obvious from the fact that gcc & clang will entirely delete a call to a pure function if the result isn't used, thereby making potential FP traps not happen, or accrued exceptions not mutated.

(also traps on exceptions would make FP ops clearly not pure by the definition in functional programming too)

> 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.

Would be perfectly fine if combined with separate NaNful and NaNless types; design the language s.t. exception handling state must always be (or is automatically set to) the necessary one, and you never get NaNs in the NaNless types.

> Floating-point operations are stateful in any CPU made during the last 40 years.

But sane programming languages entirely hide that (to the same extent that they hide integer flags and registers in general), at the very least by default; and in such sane languages, programmers do not need to be aware of any of this mess.

e.g. C's requirement of "#pragma STDC FENV_ACCESS ON" for non-default FP status, and lack thereof in 99.99% of code; C spec even explicitly notes:

> With these conventions, a programmer can safely assume default floating-point control modes (or be unaware of them). The responsibilities associated with accessing the floating-point environment fall on the programmer or program that does so explicitly.

(never mind basically every other language, which don't even allow anything other than round-nearest-ties-even + no-exceptions)

> 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.

If you want to have deterministic semantics (i.e. want to be a sane language), it absolutely does; it means that, in `a+b; x[i];`, the `x[i]` load must not happen if `a+b` results in a trap. So you cannot ever execute the next loop iteration before being sure the previous doesn't trap. Entirely forbidding autovectorization.

If you want to still have autovectorization, you'd need to have some washy semantics of like "the trap of an fp op can be delayed to an arbitrarily-later point in time, up to [insert some messy ambiguous incomplete attempt at establishing some limit to this]", which, besides making basic code non-deterministic (yikes! we don't need more of that in the world), will make proving compiler optimization correctness extra-messy.

Non-trapping exceptions (i.e. accruing) would be simpler to preserve through vectorization (allowing reordering up to reading the status, which is explicit in code), but you're back to allowing observable NaNs and making programmers insert manual checks. (and even then, you'd run into issues if you wanted to vectorize the tail of a loop on architectures without maskable FP SIMD ops)

(and, of course, unrelated to any of this, giving the programmer control over FP status means compilers are massively-restricted on optimizing code - can't DCE float ops, can't insert new FP ops even if it would be beneficial, etc)