22

I have four identity functions which do essentially nothing. Only multiplication with 1 could be optimized by clang to a single ret statement.

float id0(float x) {
    return x + 1 - 1;
}

float id1(float x) {
    return x + 0;
}

float id2(float x) {
    return x * 2 / 2;
}

float id3(float x) {
    return x * 1;
}

And the following compiler output is: (clang 10, at -O3)

.LCPI0_0:
        .long   1065353216              # float 1
.LCPI0_1:
        .long   3212836864              # float -1
id0(float):                                # @id0(float)
        addss   xmm0, dword ptr [rip + .LCPI0_0]
        addss   xmm0, dword ptr [rip + .LCPI0_1]
        ret
id1(float):                                # @id1(float)
        xorps   xmm1, xmm1
        addss   xmm0, xmm1
        ret
.LCPI2_0:
        .long   1056964608              # float 0.5
id2(float):                                # @id2(float)
        addss   xmm0, xmm0
        mulss   xmm0, dword ptr [rip + .LCPI2_0]
        ret
id3(float):                                # @id3(float)
        ret

I can understand why id0 and id2 can't be optimized. They increase the value which could then turn into positive infinity and the second operation would not change it back.

But why can't id1 be optimized? Additon with infinity would yield infinity, addition with any regular number would yield that number and addition with NaN would yield NaN. So why is it not a "true" identity operation like * 1.

Example with Compiler Explorer

Jan Schultke
  • 17,446
  • 6
  • 47
  • 96
  • This is a really good question. I don't understand why I'm the only upvoter. – Bathsheba Jun 01 '20 at 09:42
  • 1
    Not applicable to these specific examples, but please keep in mind that these results are only guaranteed by _operator associativity_. That is for example, the additive operators + and - are guaranteed to evaluate left-to-right. Therefore the implicit conversion to float happens where it should, possibly by luck(?). `x + 1 - 1` has a different meaning than `1 - 1 + x` . The former is equivalent to `(x + (float)1) - (float)1` and the latter to `(float)((int)1 - (int)1) + x;`. To avoid such bugs, use float constants `1.0f`. – Lundin Jun 01 '20 at 10:01

3 Answers3

19

IEEE 754 floating-point numbers have two zero values, one negative, one positive. When added together, the result is the positive one.

So id1(-0.f) is 0.f, not -0.f.
Note that id1(-0.f) == -0.f because 0.f == -0.f.

Demo

Also, note that compiling with -ffast-math in GCC does make the optimization and changes the result.

Nelfeal
  • 12,593
  • 1
  • 20
  • 39
7

"I have four identity functions which do essentially nothing."

That's not true.

For floating-point numbers x + 1 - 1 is not equal x + 0, it is equal (x + 1) - 1. So if you have e.g. a very small x then you will lose that very small portion in the x + 1 step, and the compiler can't know if that was your intent or not.

And in the case of x * 2 / 2, the x * 2 might not be exact either, due to floating-point precision, so you have a similar case here, the compiler does not know if you for some reason want to change the value of x in that manner.

So these would be equal:

float id0(float x) {
    return x + (1. - 1.);
}

float id1(float x) {
    return x + 0;
}

And these would be equal:

float id2(float x) {
    return x * (2. / 2.);
}

float id3(float x) {
    return x * 1;
}

The desired behavior could for sure be defined in another way. But as already mentioned by Nelfeal this optimization has to be explicitly activated using -ffast-math

Enable fast-math mode. This option lets the compiler make aggressive, potentially-lossy assumptions about floating-point math. These include:

  • Floating-point math obeys regular algebraic rules for real numbers (e.g. + and * are associative, x/y == x * (1/y), and (a + b) * c == a * c + b * c),
  • Operands to floating-point operations are not equal to NaN and Inf, and
  • +0 and -0 are interchangeable.

fast-math is for clang and gcc a collection of flags (here the one listed by clang):

  • -fno-honor-infinities
  • -fno-honor-nans
  • -fno-math-errno
  • -ffinite-math
  • -fassociative-math
  • -freciprocal-math
  • -fno-signed-zeros
  • -fno-trapping-math
  • -ffp-contract=fast
t.niese
  • 39,256
  • 9
  • 74
  • 101
  • 1
    `x * 2` might not be exact, but not because of floating point precision, rather because of its range: `x * 2` might produce an infinity if `x` is too large. This is less likely to occur for `x * 2.` as `2.` has type `double` and `double` usually has a larger range than `float`. – chqrlie Jun 01 '20 at 10:40
3

Read the floating-number-gui.de web page, more about IEEE 754, the C11 standard n1570, the C++11 standard n3337.

float id1(float x) {
    return x + 0;
}

If x happens to be a signaling NaN, your id1 might even not return (and probably should not return).

If x is a quiet NaN, then id1(x) != x since NaN != NaN (at least NaN == NaN should be false).

In some cases, you want costly arbitrary precision arithmetic. Then consider using GMPlib.

PS. Floating point numbers can give you nightmares or a PhD, at your choice. They sometimes kill people or at least make huge financial disasters (e.g. a loss of several hundred millions US$ or €).

Basile Starynkevitch
  • 223,805
  • 18
  • 296
  • 547
  • Interesting links - Especially the "kill people" link I have to bookmark. Here is another related one: https://stackoverflow.com/questions/3730019/why-not-use-double-or-float-to-represent-currency – RobertS supports Monica Cellio Jun 01 '20 at 10:24
  • *They sometimes kill people or at least make huge financial disasters (e.g. a loss of several hundred millions US$ or €)* This sounds like a personal experience, (ça sent le vécu), did you participate in the investigation on the failure of the [first Ariane 5 lauch](https://fr.wikipedia.org/wiki/Vol_501_d%27Ariane_5)? – chqrlie Jun 01 '20 at 10:30
  • 1
    If `x` is a quiet NaN, returning `x` still produces `id1(x) != x`. The real issue is negative zero where `1 / id1(x) != 1 / x` – chqrlie Jun 01 '20 at 10:31
  • @chrqlie: the lab I am working at in CEA LIST did have access to Ariane 5 related documentation, but I personally didn't. – Basile Starynkevitch Jun 01 '20 at 10:49