11

I've noticed there's a lot of discussion on the topic of floating-point computation errors which require you to use more complex comparison than ==. However, all those articles seem to be assuming the value is manipulated (or double-calculated) somehow, while I didn't see an example covering a very simple constant copying.

Please consider the following:

const double magical_value = -10;

class Test
{
    double _val;

public:
    Test()
        : _val(magical_value)
    {
    }

    bool is_special()
    {
        return _val == magical_value;
    }
};

As far as I understand this, magical_value should be set at compile time, so that all rounding occurs at that point. Afterwards, the value should just be copied to the class, and compared with the original one. Is such a comparison guaranteed to be safe? Or can either copying or comparing introduce errors here?

Please do not suggest alternative comparison or magical value use methods, that's another topic. I'm just curious about this assumption.

Edit: just to note, I am a little afraid that on some architectures, the optimizations could result in copying the value to a differently-sized floating-point registers, thus introducing differences in the exact values. Is there a risk of something like that?

Michał Górny
  • 18,713
  • 5
  • 53
  • 76
  • Will the value of `_val` be changed during runtime? – Tiago Costa Jun 13 '12 at 08:57
  • Yes, it can be changed to a real value (yet definitely `> -1`). I'm not sure if there will be a need to 'reset' it back to the special one. – Michał Górny Jun 13 '12 at 08:58
  • 1
    I would rather hope that copying a floating point value would not alter it at all, and that given two variables `a` and `b` of the same primitive type, assigning `a = b` results in `a == b` being true. However, I'm basing this assumption on common sense, which has been known to let me down in the past. – Rook Jun 13 '12 at 09:00
  • @Rook, only if the floating point is not the result of any kind of calculation. The moment you introduce calculations you start involving things like extended precision. Throw an optimizer on top of this and you can never actually know which assignments actually took place in your code. Thus comparsing anything to/from anything which _may_ have been calculated can result in precision problems. (Though of course, low-valued strict integers will always be exact) – edA-qa mort-ora-y Jun 13 '12 at 10:14
  • That's tangential to my statement. Lets say that `float a = 2f / 7f;`, it should still be the case if `float b = a;` then `b == a` should hold true, right? Which is what my comment was intended to suggest. – Rook Jun 13 '12 at 10:18

2 Answers2

2

Is such a comparison guaranteed to be safe? Or can either copying or comparing introduce errors here?

Yes, safe (this is a requirement of the copy operation as implied by =). There are no conversions/promotions that you need to worry about as long as the source and destination types are same.

However, note that magical_value may not contain 10 exactly but an approximation. This approximation will get copied over to _val.

Given the const qualifier, chances are that magical_value will probably be optimized away (should you turn on optimizations) or used as-is (i.e. no memory will probably be used up).

dirkgently
  • 108,024
  • 16
  • 131
  • 187
  • 3
    I highly doubt that there is an implementation where `10` or `-10` is not exactly representable as `double`. – Henrik Jun 13 '12 at 09:02
  • 1
    Isn't there a risk that on some architecture the value may be copied to a different-precision registers introducing potential differences? – Michał Górny Jun 13 '12 at 09:07
  • @Henrik: It's not about the value 10. Also, a lot of people tend to ignore the fact that floating point types use a mantissa-exponent based representation. – dirkgently Jun 13 '12 at 09:08
  • 1
    @MichałGórny: `magical_value` is a `const` and will probably never see the light of day (constant propagation). What remains then to be checked is what happens with `_val`. [1] Given that `_val` may or may not hold the value of `magical_value` you cannot rely on `==` for equality testing. [2] Note that you're using a the *native* floating-point type i.e. `double` (whose actual size may vary across architectures). `double` (and `int`) are the promoted-to types as they are the best abstractions that you'd get in the standard for registers. That should put to rest any register related issues. – dirkgently Jun 13 '12 at 09:16
  • @MicthałGórny: What I mean is this: `==` works fine on a copy. The semantics of copy guarantee this. However, if `_val` can have another value (say assigned via some expression) then of course you cannot count on `==` alone but will need to account for floating point differences. – dirkgently Jun 13 '12 at 09:41
  • @dirkgently I think this is not entirely correct in case of denormalized floating point. see my answer below. – mvds Jun 13 '12 at 09:53
  • @mvds: Denormalized floating point literal assignment to an object of type `double`/`float` may not be exact. However, once this assignment is done all copies will be identical. Your code deals with `float`s. They *are* promoted to double. That is the first place where you're likely to go off. `-ffast-math` is a disaster waiting to happen when you want stable operations. Holes around zero can lead to problem on any representation. – dirkgently Jun 13 '12 at 10:09
  • @dirkgently: just to let you know, I verified the example still holds for doubles. The constant only needs to be `0.00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002225` which is the reason why for the proof of concept I test these things with floats (less zeroes). – mvds Jun 13 '12 at 10:48
  • @dirkgently: oh and btw, some environments have flush to zero enabled by default, see http://stackoverflow.com/questions/9350810/denormalized-floating-point-in-objective-c/9350820#9350820 for an analysis of iOS on ARMv7. – mvds Jun 13 '12 at 10:50
  • -1. See my answer, the copy operation is not 100% safe as stated here and the "no conversions/promotions to worry about" statement is not true. – mvds Jun 14 '12 at 08:24
  • @mvds: You're missing the point entirely. We are discussing `a==c` after `a = c`. This *will* be the same since this is a byte copy. – dirkgently Jun 14 '12 at 09:05
  • Agreed about `a==c` after `a=c`, but that misses the point I'm making. What's the use of a magic value if the magic value might have 2^52-1 siblings which are equally magic? Let's call it a *magic range* or *magic subset* then. – mvds Jun 14 '12 at 09:48
0

Apart from possibly different-sized registers, you have denormalized floating point (cq flush-to-zero) to worry about (see Why does changing 0.1f to 0 slow down performance by 10x?)

Just to give an idea of the weirdness this could lead to, try this bit of code:

float       a = 0.000000000000000000000000000000000000000047683384;
const float b = 0.000000000000000000000000000000000000000047683384;
float aa = a, bb = b;

#define SUPPORT_DENORMALIZATION ({volatile double t=DBL_MIN/2.0;t!=0.0;})

printf("support denormals: %d\n",SUPPORT_DENORMALIZATION);
printf("a = %.48f, aa = %.48f\na==aa %d, a==0.0f %d, aa==0.0f %d\n",a,aa,a==aa,a==0.0f,aa==0.0f);
printf("b = %.48f, bb = %.48f\nb==bb %d, b==0.0f %d, bb==0.0f %d\n",b,bb,b==bb,b==0.0f,bb==0.0f);

which gives either: (compiled without flush-to-zero)

support denormals: 1
a = 0.000000000000000000000000000000000000000047683384, aa = 0.000000000000000000000000000000000000000047683384
a==aa 1, a==0.0f 0, aa==0.0f 0
b = 0.000000000000000000000000000000000000000047683384, bb = 0.000000000000000000000000000000000000000047683384
b==bb 1, b==0.0f 0, bb==0.0f 0

or: (compiled with gcc -ffast-math)

support denormals: 0
a = 0.000000000000000000000000000000000000000000000000, aa = 0.000000000000000000000000000000000000000000000000
a==aa 1, a==0.0f 1, aa==0.0f 1
b = 0.000000000000000000000000000000000000000047683384, bb = 0.000000000000000000000000000000000000000000000000
b==bb 1, b==0.0f 0, bb==0.0f 1

Where that last line is of course the odd one out: b==bb && b!=0.0f && bb==0.0f would be true.

So if you're still thinking about comparing floating point values, at least stay away from small values.

update to offset some comments about this being due to the use of floats instead of doubles, it also works for double, but you would need to set the constant to somewhere below DBL_MIN, e.g. 1e-309.

update 2 a code sample relating to some comments made below. This shows that the problem exists for doubles as well, and that comparisons can become inconsistent (when flush to zero is enabled)

    double a;
    const double b = 0.00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001225;
    const double c = 0.00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002225;

    printf("b==c %d\n",b==c);
    a = b;
    printf("assigned a=b: a==b %d\n",a==b);
    a = c;
    printf("assigned a=c: a==b %d\n",a==b);

output:

b==c 0
assigned a=b: a==b 1
assigned a=c: a==b 1

The issue shows in the last line, where you would naively expect that a==b would become false after assigning a=c with c!=b.

Community
  • 1
  • 1
mvds
  • 45,755
  • 8
  • 102
  • 111
  • -1. See my comment in my answer. I've already pointed out enough issues with this piece of code. I don't really see the point you're trying to prove. – dirkgently Jun 13 '12 at 10:13
  • @dirkgently dude, relax. The point is: the OP's idea that floating point may have some hidden "features" is not misplaced. – mvds Jun 13 '12 at 10:42
  • I don't see how this is a problem. In all your tests, `a==aa` and `b==bb` were true, so what the OP is doing is safe. You did show some inconsistencies when comparing with zero, but that is a separate issue from what the OP is doing. – interjay Jun 13 '12 at 11:01
  • 1
    Have to agree that this is interesting but irrelevant: OP basically wants to know if, given `double a = X; ... double b = a;` then `a==b` is always true for all values of X. The answer's yes. – Roddy Jun 13 '12 at 11:09
  • @interjay: the same holds for comparisons between denormals, not just zeroes. @Roddy: yes, `a==b` is always true for all values of X, as is `a==c` with some `c` where `c!=b`. The point is, testing `a==b` implies you want to know if either a or b changed, which you cannot do 100% reliably in some edge cases. Mix in optimization flags and all hope is lost. – mvds Jun 13 '12 at 12:49
  • +1 That's interesting. I've never considered the consequences of constant-propagation through flush-to-zero mode. If the compiler isn't aware that FTZ is enabled, it could produce erroneous constant-propagation. – Mysticial Jun 13 '12 at 13:33
  • @mvds - I think OP is setting `_val` to a magic number to indicate some special state (eg, 'uninitialized'), and wants to check that `_val` is still in that state at a later point. This requires that the range of valid 'actual' values is a well defined subrange of all reals, and that your magic number is chosen to be well outside this subgrange.if your range of valid reals was 0 to 10.0, then 10.000000000001 could be a poor choice of magic number. – Roddy Jun 13 '12 at 15:42
  • @Roddy: I agree that in practice, there will probably be no problem. But this is a *theoretical* discussion about a language and its various implementations on different kinds of hardware. "Guaranteed to be safe for *all* X" is not true if it is *not* safe for one in 2^9 possible float values or one in 2^12 possible double values. Compare it to the situation for any form of integer, where you probably *can* get the 100% safety guarantee. – mvds Jun 14 '12 at 08:20
  • Just to clarify, is this at all relevant to the particular number range I used in the example? Because very small negative values already fall into the range of computation error and thus I wouldn't even consider using such a value for the 'magical' one. Thinking about it more, I probably will be better with something being a power of two, right? – Michał Górny Jun 14 '12 at 16:06
  • @MichałGórny: As long as you stay above DBL_MIN (or FLT_MIN) you should be safe. – mvds Jun 14 '12 at 21:18