Floating point equality


Floating point equality



It is common knowledge that one has to be careful when comparing floating point values. Usually, instead of using ==, we use some epsilon or ULP based equality testing.


==



However, I wonder, are there any cases, when using == is perfectly fine?


==



Look at this simple snippet, which cases are guaranteed to succeed?


void fn(float a, float b) {
float l1 = a/b;
float l2 = a/b;

if (l1==l1) { } // case a)
if (l1==l2) { } // case b)
if (l1==a/b) { } // case c)
if (l1==5.0f/3.0f) { } // case d)
}

int main() {
fn(5.0f, 3.0f);
}



Note: I've checked this and this, but they don't cover (all of) my cases.



Note2: It seems that I have to add some plus information, so answers can be useful in practice: I'd like to know:



This is the only relevant statement I found in the current draft standard:



The value representation of floating-point types is implementation-defined. [ Note: This document imposes no requirements on the accuracy of floating-point operations; see also [support.limits]. — end note ]



So, does this mean, that even "case a)" is implementation defined? I mean, l1==l1 is definitely a floating-point operation. So, if an implementation is "inaccurate", then could l1==l1 be false?


l1==l1


l1==l1



I think this question is not a duplicate of Is floating-point == ever OK?. That question doesn't address any of the cases I'm asking. Same subject, different question. I'd like to have answers specifically to case a)-d), for which I cannot find answers in the duplicated question.





@interjay: yes, this is what I think too, but I'm not 100% sure. IEEE 754 mandates this, but I don't know what does the C++ standard say about this, if says anything at all.
– geza
Jul 2 at 10:36






There is no guarantee at all.
– Sombrero Chicken
Jul 2 at 10:45





Related: stackoverflow.com/q/8044862/560648
– Lightness Races in Orbit
Jul 2 at 11:40





The very answer you'll ever need: Never ==.
– iBug
Jul 2 at 14:21


==





@iBug: can you explain why?
– geza
Jul 2 at 18:18




6 Answers
6



However, I wonder, are there any cases, when using == is perfectly fine?



Sure there are. One category of examples are usages that involve no computation, e.g. setters that should only execute on changes:


void setRange(float min, float max)
{
if(min == m_fMin && max == m_fMax)
return;

m_fMin = min;
m_fMax = max;

// Do something with min and/or max
emit rangeChanged(min, max);
}



See also Is floating-point == ever OK? and Is floating-point == ever OK?.





It's not about that.
– Sombrero Chicken
Jul 2 at 11:13





@SombreroChicken: Why not?
– Lightness Races in Orbit
Jul 2 at 11:41





@SombreroChicken: What's the difference?
– Lightness Races in Orbit
Jul 2 at 11:43





It correctly or not, it does answer this bit "I wonder, are there any cases, when using == is perfectly fine?"
– luk32
Jul 2 at 11:50






@luk32 x86 FP stack have bigger precision than stored value. ABI on x86 allow passing floats in FP stack. Now you choose value that is greater in precision than memory float but still can be stored in FP stack. when you do m_max = arg; it will truncate value. then every test m_max == arg will fall because you check in reality trunc(arg) == arg.
– Yankes
Jul 2 at 13:52


m_max = arg;


m_max == arg


trunc(arg) == arg



Contrived cases may "work". Practical cases may still fail. One additional issue is that often optimisation will cause small variations in the way the calculation is done so that symbolically the results should be equal but numerically they are different. The example above could, theoretically, fail in such a case. Some compilers offer an option to produce more consistent results at a cost to performance. I would advise "always" avoiding the equality of floating point numbers.



Equality of physical measurements, as well as digitally stored floats, is often meaningless. So if your comparing if floats are equal in your code you are probably doing something wrong. You usually want greater than or less that or within a tolerance. Often code can be rewritten so these types of issues are avoided.





Not always, see user2301274's answer. Equality can be used the check for a variable change. For example, if a set function uses a value which is the same as the current value, it can return right away.
– geza
Jul 2 at 11:51


set





@geza Or, more generally, float equality implies "real-world" equality, but the other way around is not necessarily true.
– Yogu
Jul 2 at 16:52





@geza: But it's pretty easy to contrive cases where that set function wouldn't work. E.g. originally set to 1.0, later set to a computed value that's actually 1.(enough zeros for precision)1.
– jamesqf
Jul 2 at 22:56





"often meaningless" Than what is guaranteed to be meaningful?
– curiousguy
Jul 3 at 17:03





@curiousguy - a meaningful solution is one that actually meets the real formal requirements that you have distilled or mined from the customer's brief. Other than for the investigation of computer science, if you are coding if(length_of_wood == 3.2), your formal requirements are probably wrong. The brief may be that the wood is 3.2m long. Coding this is "meaningless". The formal requirement may be something like, the measured length of the wood should be between 3.19 and 3.21m. Meaningful is meeting the customer brief. (NOT coding the brief as though they are requirements.)
– William J Bagshaw
Jul 4 at 9:42



Only a) and b) are guaranteed to succeed in any sane implementation (see the legalese below for details), as they compare two values that have been derived in the same way and rounded to float precision. Consequently, both compared values are guaranteed to be identical to the last bit.


float



Case c) and d) may fail because the computation and subsequent comparison may be carried out with higher precision than float. The different rounding of double should be enough to fail the test.


float


double



Note that the cases a) and b) may still fail if infinities or NANs are involved, though.



Using the N3242 C++11 working draft of the standard, I find the following:



In the text describing the assignment expression, it is explicitly stated that type conversion takes place, [expr.ass] 3:



If the left operand is not of class type, the expression is implicitly converted (Clause 4) to the cv-unqualified type of the left operand.



Clause 4 refers to the standard conversions [conv], which contain the following on floating point conversions, [conv.double] 1:



A prvalue of floating point type can be converted to a prvalue of another floating point type. If the
source value can be exactly represented in the destination type, the result of the conversion is that exact
representation. If the source value is between two adjacent destination values, the result of the conversion
is an implementation-defined choice of either of those values.
Otherwise, the behavior is undefined.



(Emphasis mine.)



So we have the guarantee that the result of the conversion is actually defined, unless we are dealing with values outside the representable range (like float a = 1e300, which is UB).


float a = 1e300



When people think about "internal floating point representation may be more precise than visible in code", they think about the following sentence in the standard, [expr] 11:



The values of the floating operands and the results of floating expressions may be represented in greater
precision and range than that required by the type; the types are not changed thereby.



Note that this applies to operands and results, not to variables. This is emphasized by the attached footnote 60:



The cast and assignment operators must still perform their specific conversions as described in 5.4, 5.2.9 and 5.17.



(I guess, this is the footnote that Maciej Piechotka meant in the comments - the numbering seems to have changed in the version of the standard he's been using.)



So, when I say float a = some_double_expression;, I have the guarantee that the result of the expression is actually rounded to be representable by a float (invoking UB only if the value is out-of-bounds), and a will refer to that rounded value afterwards.


float a = some_double_expression;


float


a



An implementation could indeed specify that the result of the rounding is random, and thus break the cases a) and b). Sane implementations won't do that, though.





Comments are not for extended discussion; this conversation has been moved to chat.
– Samuel Liew
Jul 5 at 11:11



Assuming IEEE 754 semantics, there are definitely some cases where you can do this. Conventional floating point number computations are exact whenever they can be, which for example includes (but is not limited to) all basic operations where the operands and the results are integers.



So if you know for a fact that you don't do anything that would result in something unrepresentable, you are fine. For example


float a = 1.0f;
float b = 1.0f;
float c = 2.0f;
assert(a + b == c); // you can safely expect this to succeed



The situation only really gets bad if you have computations with results that aren't exactly representable (or that involve operations which aren't exact) and you change the order of operations.



Note that the C++ standard itself doesn't guarantee IEEE 754 semantics, but that's what you can expect to be dealing with most of the time.





@Ben oops, yeah I've been editing that example several times. Ended up removing it, you're right even if I fixed it it wouldn't add that much.
– Cubic
Jul 3 at 7:22



Case (a) fails if a == b == 0.0. In this case, the operation yields NaN, and by definition (IEEE, not C) NaN ≠ NaN.


a == b == 0.0



Cases (b) and (c) can fail in parallel computation when floating-point round modes (or other computation modes) are changed in the middle of this thread's execution. Seen this one in practice, unfortunately.



Case (d) can be different because the compiler (on some machine) may choose to constant-fold the computation of 5.0f/3.0f and replace it with the constant result (of unspecified precision), whereas a/b must be computed at runtime on the target machine (which might be radically different). In fact, intermediate calculations may be performed in arbitrary precision. I've seen differences on old Intel architectures when intermediate computation was performed in 80-bit floating-point, a format that the language didn't even directly support.


5.0f/3.0f


a/b





"changed in the middle of this thread's execution" Do you mean changed by one thread with impact on others?
– curiousguy
Jul 4 at 5:41





@curiousguy — Sadly, yes. Specifically, this was on Intel architecture, where the D3D library changed the CPU's floating-point round mode and didn't reset it. Since the FP mode was a property of the CPU, it affected all threads. Now, the likelyhood of this happening is low, but it has happened to me.
– Steve Hollasch
Jul 5 at 18:34



In my humble opinion, you should not rely on the == operator because it has many corner cases. The biggest problem is rounding and extended precision. In case of x86, floating point operations can be done with bigger precision than you can store in variables (if you use coprocessors, IIRC SSE operations use same precision as storage).


==



This is usually good thing, but this causes problems like:
1./2 != 1./2 because one value is form variable and second is from floating point register. In the simplest cases, it will work, but if you add other floating point operations the compiler could decide to split some variables to the stack, changing their values, thus changing the result of the comparison.


1./2 != 1./2



To have 100% certainty you need look at assembly and see what operations was done before on both values. Even the order can change the result in non-trivial cases.



Overall what is point of using ==? You should use algorithms that are stable. This means they work even if values are not equal, but they still give the same results. The only place I know where == could be useful is serializing/deserializing where you know what result you want exactly and you can alter serialization to archive your goal.


==


==





Comments are not for extended discussion; this conversation has been moved to chat.
– Samuel Liew
Jul 4 at 1:43






By clicking "Post Your Answer", you acknowledge that you have read our updated terms of service, privacy policy and cookie policy, and that your continued use of the website is subject to these policies.

Popular posts from this blog

How to make file upload 'Required' in Contact Form 7?

Rothschild family

amazon EC2 - How to make wp-config.php to writable?