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
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.
"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
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 €).
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With