Haskell is not about removing side effects in the first place-- it's about making them obvious and contained, and about declaring them. It's about the type system imposing certain constraints on what's allowed and where: none of the above really applies to C.
sin(±infinity) returns a NaN and raises the "invalid"
floating-point exception.
However, I can't figure out how to make that happen, and my clumsy attempts to figure out <fenv.h> and feraiseexcept() failed to make sin(INFINITY) do something other than return a NaN.Some architectures (but not all) do include some bits in the floating-point control register that will cause a hardware trap to be generated on an exception. There's no standard C function to do this, and even compiler builtin support is kind of sketchy, so your best bet is usually resorting to inline assembly to do this.
The strict model requires that everything be evaluated at runtime and that the optimizer be aware of the potential for floating-point operations to have interactions with floating-point control and status words (i.e., rounding modes and fp exceptions)--essentially all optimizations are floating-point are forbidden.
The precise model allows compilers to ignore the dependency on rounding mode and fp exceptions, and treat floating-point optimizations as side-effect-free pure expressions, but still requires them to generally follow IEEE 754 arithmetic. As such, standard optimizations like code motion or constant propagation are legal, but arithmetic optimizations (like replacing x + 0.0 with x) are not generally legal.
The fast model allows compilers to also apply the arithmetic optimizations.
Most people who actually care about this level of detail are aware of the panoply of compiler options that are used to control floating-point optimization in the compiler.
Edit: Hee-hee! And if I put my source into Godbolt's Compiler Explorer, compile with gcc 12.2 -O3 -ffast-math then my program turns into:
main:
xor eax, eax
ret(Speaking of fast math, my planned blog post title if I ever write all this up into a blog post is "Floating Point, or How I Learned to Start Worrying and Hate Fast Math.")
More precisely, my test compared the bit-patterns directly:
i = 0 .. 2^32-1
memcpy(&x, &i, 4);
y = x + 0.0f;
memcpy(&j, &y, 4)
if (i != j) {printf("%f + 0.0 -> %f\n", x, y);
and I'm pretty sure that NaN addition, while resulting in a NaN, does not need to preserve the original bit pattern.Regarding fast-not-so-math - Praise Kahan!
"it be complicated"
Except for certain operations (fneg, fcopysign, and fabs are all defined to only affect the sign bit, to the point that they're an exception to the sNaN-always-signals rule; and there are a few operations defined to operate on the NaN payload information), IEEE 754 generally says no more than a qNaN is generated. There's a recommendation that NaN payloads propagate, and most hardware does do that, but compilers actually typically don't even guarantee that sNaNs are not produced by operations.
I looked more closely, and yes, the signalling NaN + 0.0 always changed the bit pattern, and the quiet NaN + 0.0 always stayed the same.
Thank you for your informative details about IEEE 754.
(edit), for instance this gives me output of "1":
#include <math.h>
#include <fenv.h>
#include <stdio.h>
int main(void)
{
double a = sin(INFINITY);
printf("%d\n", fetestexcept(FE_INVALID));
return 0;
}I think I have an outdated idea that there should be a way to have the exception trigger a SIGFPE, and that modern processors don't do that.
If you care about it, you need to use the appropriate flags to get a strict floating-point model. If you merely have a precise or fast floating-point model, calling fetestexception is going to result in unspecified behavior.
[If you really want to be strict, the result of flags are unspecified if you don't use #pragma FENV_ACCESS ON, but gcc refuses to support FENV_ACCESS pragma, and you instead need to use command-line flags to get the same effect.]
> I think I have an outdated idea that there should be a way to have the exception trigger a SIGFPE, and that modern processors don't do that.
On Intel, you can clear the exception mask for the INVALID FP exception in mxcsr and get an SIGFPE whenever an operation raises INVALID [assuming, again, strict model to ensure the optimizer respects fp exceptions.] I don't know how, or if, AArch64 can enable hardware traps on floating-point exceptions.
Although, in that case, this function would still be pure as far as Haskell is concerned. Because its output only depends on its input. Interrupts/exceptions are outside of the scope of the type system as far as an individual function goes.
And testing just now, I can't even trigger SIGFPE with division by zero (integer or float). I guess all of these exceptions are "caught" now.
a.out: Mach-O 64-bit executable arm64
while my gcc-12 compiles to: a.out: Mach-O 64-bit executable x86_64
which is then emulated via Rosetta.... can it? I don't see it in the man page. The rest is a good point, either way, though.