Why does divison by 0 always result in negative NaN?

Started by
45 comments, last by Sik_the_hedgehog 9 years, 7 months ago

I'm sure there are cases where bad things occur, and I certainly don't mean to disagree with those points, but it's often a clearly defined operation, and Infinity and NaN can both be acceptable floating point values. For game development I would consider the assumption of division by zero being OK perfectly reasonable, unless targeting a specific platform where it's known to be a problem.
Under that assumption, '0 / 0' is also an exceptionally exceptional case, whereas 'anything else / 0' is infinity, and identical to 'very large / very small' which is also infinity even though both the numerator and denominator are valid finite values. Division by zero is not the only way to get Infinity or NaN, and several standard math functions often return these values (log / sqrt etc).
When doing sequential operations on floats that may or may not end up as zero, checking for edge-cases between every single operation is not really desirable.. and checking the final result for Inf/NaN is probably better. If aiming for SIMD it's very desirable to not have to worry about it.


Division of 0 by 0 does not result in infinity (if you think it does, graph the approach to it from positive and negative numbers). In the IEEE floating point case it results in NaN.

The undefined behavior essentially involves any case whereby you make the assumption that the denominator is not 0 and then later act upon the denominator being 0. As shown in the linked clang post and the previous post to yours. Most compilers, if you attempt to write out an immediate divide by zero using constants, will display an error or warning. However, when you have variables whose value might be zero and you make a code assumption that they are NOT zero, then later attempt to perform conditional actions dependent upon it being zero the compiler may (or may not, it is after all undefined behavior) optimize out those sections as you did, after all, tell it that it was perfectly fine before.

In time the project grows, the ignorance of its devs it shows, with many a convoluted function, it plunges into deep compunction, the price of failure is high, Washu's mirth is nigh.

Advertisement

Just to make it clear: the question was not about the compiler (I know that dividing by zero could result in the universe imploding if the compiler decided so), it was about why the FPU would ever return -NaN instead of NaN in the first place.

I made a simpler program and checked the assembly code to be 100% sure this time:

    movl    .LC0(%rip), %eax
    movl    %eax, -12(%rbp)
    movl    .LC0(%rip), %eax
    movl    %eax, -8(%rbp)
    movss    -12(%rbp), %xmm0
    divss    -8(%rbp), %xmm0
    movss    %xmm0, -4(%rbp)

This results in -NaN, so yeah, it's definitely the FPU doing it.

So the question is why would it generate -NaN and not +NaN? Like, is there any practical reason from a hardware viewpoint for it or what? (also amusingly, if I set optimizations to max with that program I get +NaN, though in this case the compiler is using a constant so the FPU isn't involved at all)

Don't pay much attention to "the hedgehog" in my nick, it's just because "Sik" was already taken =/ By the way, Sik is pronounced like seek, not like sick.

Just to make it clear: the question was not about the compiler (I know that dividing by zero could result in the universe imploding if the compiler decided so), it was about why the FPU would ever return -NaN instead of NaN in the first place.

I made a simpler program and checked the assembly code to be 100% sure this time:

    movl    .LC0(%rip), %eax
    movl    %eax, -12(%rbp)
    movl    .LC0(%rip), %eax
    movl    %eax, -8(%rbp)
    movss    -12(%rbp), %xmm0
    divss    -8(%rbp), %xmm0
    movss    %xmm0, -4(%rbp)
This results in -NaN, so yeah, it's definitely the FPU doing it.

So the question is why would it generate -NaN and not +NaN? Like, is there any practical reason from a hardware viewpoint for it or what? (also amusingly, if I set optimizations to max with that program I get +NaN, though in this case the compiler is using a constant so the FPU isn't involved at all)


Because it can? Both are valid, so its up to the processor vendor to decide.

In time the project grows, the ignorance of its devs it shows, with many a convoluted function, it plunges into deep compunction, the price of failure is high, Washu's mirth is nigh.


Division by 0 does not result in infinity (if you think it does, graph the approach to it from positive and negative numbers). In the IEEE floating point case it results in NaN.

The Wikipedia entry specifically states that it can return infinity, though I'm not that familiar with the subtleties to know if it will always be the case. Experimental results in VC++ show infinity.


for(uint32_t i = 5; i > 0; --i) {
	uint32_t uival = i - 1;
	float fval = *reinterpret_cast<float*>(&uival);
	float fres = 1.0f / fval;

	std::cout << "1.0f / " << fval << " = " << fres << std::endl;
}

The undefined behavior example is definitely a good one, and makes sense.

I would however still be most upset if a program compiled to perform divisions on floating point values would optimize out a later statement depending on the denominator, just because it was used in an earlier operation, if that operation is completely valid and the denominator is unknown.

There is the case of floating point exceptions, but those can be triggered by many more things than division by zero, such as denormal values and overflow.

I'm not entirely sure we're discussing the same thing though.. again I don't think I disagree with the points raised.. I just feel that if the compiler started assuming floating points denominators are never zero it could probably cause a lot more subtle problems than optimizing out code like that..

It is not true that floating-point division by zero results in undefined behavior. If your compiler and library follow IEEE-754, 1.0/0.0 is Infinity and 0.0/0.0 is some sort of NaN. I believe until recently my libc implementation would not print the sign of a NaN, but now it does.

If I am wrong about this, can someone please point to the relevant paragraph in the standard?

The Wikipedia entry specifically states that it can return infinity, though I'm not that familiar with the subtleties to know if it will always be the case. Experimental results in VC++ show infinity.

Yes, if you divide a number A by 0 where A is NOT +/- 0 then you will get +/-infinity in IEEE floats. I should have been clearer in my statement as I was dealing with 0/0. In mathematics lim x->0 1/x diverges.

The undefined behavior example is definitely a good one, and makes sense.
I would however still be most upset if a program compiled to perform divisions on floating point values would optimize out a later statement depending on the denominator, just because it was used in an earlier operation, if that operation is completely valid and the denominator is unknown.
There is the case of floating point exceptions, but those can be triggered by many more things than division by zero, such as denormal values and overflow.

I'm not entirely sure we're discussing the same thing though.. again I don't think I disagree with the points raised.. I just feel that if the compiler started assuming floating points denominators are never zero it could probably cause a lot more subtle problems than optimizing out code like that..

If you tell the compiler that it can make some assumptions (such as by doing a / b without testing if b is zero first), then the compiler can only assume that since you desire a well defined program b must not be zero. As such any condition that relies upon b being zero AFTER a / b must clearly be false.

In time the project grows, the ignorance of its devs it shows, with many a convoluted function, it plunges into deep compunction, the price of failure is high, Washu's mirth is nigh.


If you tell the compiler that it can make some assumptions (such as by doing a / b without testing if b is zero first), then the compiler can only assume that since you desire a well defined program b must not be zero

My point is that a program where b is sometimes but not always zero is still well-formed, under normal floating point conditions. If that is strictly incorrect then please enlighten me, but I really don't see that being the case.

If you tell the compiler that it can make some assumptions (such as by doing a / b without testing if b is zero first), then the compiler can only assume that since you desire a well defined program b must not be zero


My point is that a program where b is sometimes but not always zero is still well-formed, under normal floating point conditions. If that is strictly incorrect then please enlighten me, but I really don't see that being the case.

Well defined:


if(b == 0) {
    someFlag = true;
    return;
}
result = a / b;

Not so well defined:


result = a / b;

if(b == 0) {
    someFlag = true;
    return;
}

The problem here is that result = a / b tells the compiler that "b must not be zero, because that would be UB." Because the compiler, since it assumes your program is WELL DEFINED, can now assume the invariant b != 0, then the latter if statement if(b == 0) becomes if(false).

In time the project grows, the ignorance of its devs it shows, with many a convoluted function, it plunges into deep compunction, the price of failure is high, Washu's mirth is nigh.

I understand the logic, and for integers I'm agreeing without reservations.

I also accept that writing code like that might a bad idea, even for floats.

However, for floating point I believe that the assertion does not hold, and if the 'if(b == 0)' statement can be hit at all (no guaranteed exception on division by zero), it will not be optimized out, and the program is still very well defined. The compiler will not and can not make such assumptions.

I apologize if I'm making assumptions in my argument that renders the point moot, but for the question to make sense at all we have to treat the standard as "C++ + float", where float is some floating point specification. in all cases I've ever encountered it's more or less IEEE (though IEEE in itself has implementation-defined behavior).

Example, that I would assume is a completely valid well-formed program under C++ + IEEE:



#include <iostream>
#include <cfloat>
#include <cstdlib>

bool checkZeroDivZero(float f0, float f1) {
	float result = f0 / f1;

	if(f1 == 0.0f || f1 == -0.0f) {
		if(isnan(result))
			return true;
	}

	return false;
}

int main() {
	for(int i = 0; i < 25; ++i) {
		float f0 = static_cast<float>(rand() % 3);
		float f1 = static_cast<float>(rand() % 3);

		if(checkZeroDivZero(f0, f1))
			std::cout << "0 / 0" << std::endl;
		else
			std::cout << "x / y" << std::endl;
	}

	return 0;
}

EDIT: Being well-formed in this case ofcourse does not mean being deterministic at run-time, as the result from floating point operations can change even from call to call to the same function, if the floating-point mode has been changed (such as D3D automatically changing it, as many of us have probably encountered).


I understand the logic, and for integers I'm agreeing without reservations.

I also accept that writing code like that might a bad idea, even for floats.



However, for floating point I believe that the assertion does not hold, and if the 'if(b == 0)' statement can be hit at all (no guaranteed exception on division by zero), it will not be optimized out, and the program is still very well defined. The compiler will not and can not make such assumptions.

I apologize if I'm making assumptions in my argument that renders the point moot, but for the question to make sense at all we have to treat the standard as "C++ + float", where float is some floating point specification. in all cases I've ever encountered it's more or less IEEE (though IEEE in itself has implementation-defined behavior).

This has nothing to do with the IEEE standard. This has to do with the C++ standard:


5.6 - 4

The binary / operator yields the quotient, and the binary % operator yields the remainder from the division
of the first expression by the second. If the second operand of / or % is zero the behavior is undefined. For
integral operands the / operator yields the algebraic quotient with any fractional part discarded;80 if the
quotient a/b is representable in the type of the result, (a/b)*b + a%b is equal to a.

In time the project grows, the ignorance of its devs it shows, with many a convoluted function, it plunges into deep compunction, the price of failure is high, Washu's mirth is nigh.

This topic is closed to new replies.

Advertisement