26 comments

[ 1.9 ms ] story [ 83.7 ms ] thread
> If a warning message is considered not fixable, or not desirable to fix, it’s preferable to remove the associated flag from the build chain.

I like to use diagnostic pragmas (https://gcc.gnu.org/onlinedocs/gcc/Diagnostic-Pragmas.html) in the vanishingly rare cases where I can't fix the cause of a warning.

This unfortunately gets messy when you're targeting multiple compilers, and in some cases multiple versions of the same compiler.
At least clang has it covered: https://clang.llvm.org/docs/UsersManual.html#pragma-gcc-diag...

Don't know what the situation is on MS's and Intel's compilers.

And you can use _Pragma / __pragma to hide them inside (conditional) macros without #ifdefing up every call site.

There are still edge cases where you can't sanely scope them relating to when templates are evaluated, being unable to use the pragmas at arbitrary points of an expression, warnings that don't respect the warning flags properly, etc. - but it works most of the time.

That said, for warnings with high false positives and low value true positives, I'll just globally suppress the warning.

Works well if your only targets are GCC and Clang. For building across several platforms with other compilers, you could just manipulate the flags globally on those other compilers without giving up high levels and specificity of warnings on gcc/clang.
> -Wdeclaration-after-statement : this flag is useful for C90 compatibility.

This option is rather for developers who want to avoid declarations after statements in C dialects that support them.

If you want to enforce actual C90 compatibility use -ansi or -std=c90. Otherwise you just have partial compatibility which is actually incompatibility.

Some context: the author is Yann Collet, the principal author of zstd, lz4, and xxHash. In my experience he is an extremely talented and friendly engineer.
All of this seems fine, except for the floating point but.

Floating point is completely deterministic, and has a very clearly defined set of behaviours.

I don't know much about the C standard, but the article states that implementation details are platform-specific. If that's the case, it's a very sensible suggestion.

Sure, if the same system runs the same operation twice, the two values will be equal. But what if you're comparing to a stored value generated on a different system?

It doesn't even need to be a different system. Different compilations of the same program on the same system can result in different floating point results (either because of internal high-precision floating point registers being utilized differently, or the compiler making algebraically valid re-arrangements that change the floating point result).

Additionally, many usages of == assume basic algebraic properties of numbers to hold.

It is possible to use equality with floats, but require far more care than is typically worth it.

One option is to have an ExactlyEquals function or macro that wraps the == in pragmas to supress the warning for the scope. At work some of our types don't even define == just to force the issue.
If the optimization level changes the behaviour the compiler is broken. “Optimizing” by assuming commutivity or associativity of floating point is no less incorrect than optimizing by assuming string addition was commutativeand associative.
There is no requirement that optimization does not change behaviour. The only requirement is that the optimized behavior remains consistent with the standards. The point of the point on floating point equality is that the standards give enough room for variance that you should not use literal equality unless you know exactly what you are doing and have a good reason for doing it.

As an aside, if your concern is that the standards impose too much restrictions, another popular option is the ffast-math option.

We see this come up with strings fairly often in higher level languages where, sometimes, logical equal strings will happen to be equal because the runtime's interning system made them point to the same location, but there is no guarantee this would happen and the implementation is free to change it at any point. I believe I have also seen this behaviour come up with interning on some Integer wrapper types.

> I believe I have also seen this behaviour come up with interning on some Integer wrapper types.

Python, for example:

  >>> 8 is 8
  True
  >>> 8**1000 is 8**1000
  False
Because python is broken in this respect -- JS engines do exactly the same object optimisations and get comparisons correct.
Meh. They're different languages with different expectations. "is" is distinct from "==" in both, and Python gets "==" right (obviously). Javascript doesn't really have any high ground here[0]. (In contrast, Python:)

  >>> [] == False
  False
  >>> object() == False
  False
  >>> "" == False
  False
  >>> {} == False
  False
They're just different.

[0]: https://www.youtube.com/watch?v=et8xNAc2ic8

JS has "strict equality" comparison. It's not the same as "identity" comparison.
No, optimizations are allowed to change the ordering or exact implementations, but they cannot change observable behavior unless the behavior depends on undefined (e.g the compiler is the adversary) or unspecified (in which the compiler is required to be self-consistent, e.g parameter evaluation order is (was?) unspecified so a compiler could choose left to right or right to left evaluation, but it must pick one and use that everywhere).

The C abstract machine defines the behavior, and optimization passes aren't allowed to change the behavior.

-ffast-math is the option that allows incorrect semantics (e.g commutative floating point addition), but compilers have a desire to do incorrect things for "performance".

I would hope it is obvious that changing behavior for performance isn't ok, but lets say it was. A compiler could change

y=1.0/fsqrt(x)

from (v. approximate, untested, and written in a text box :D)

movsd xmm0, $(wherever x is) sqrtsd xmm0 movsd xmm1, $(location of 1.0 constant) divsd xmm1, xmm0 movsd $(addr of y), xmm1

to something like

movsd xmm0, $(wherever x is) rsqrtsd xmm0 movsd $(addr of y), xmm0

This would be much much faster, and vastly incorrect (IIRC rsqrt is only 12bits of precision)

Or even more dumb, remove calls to printf, because printf is slow.

A compiler does not have absolute leeway in all things, it is still restricted to ensuring that observable results of the C abstract machine are correct.

(Well string addition should be associative)
Hurrr derp :D

You are of course right :)

No, you will get the same results on all systems - ieee754 defines exact semantics and behavior.

The problems stem from running under different rounding modes on different systems, I believe at this point literally all platforms default to RTE, and if you’re working with a different rounding mode your errors are going to be much more significant than “compare to epsilon”.

The historic problems people have had with C compilers and floating point is that compiler vendors seem hell bent on “improving” performance by effectively changing clearly intended semantics (eg the compiler is an adversary). These include performing calculations at an incorrect precision (eg host precision, “convenient” precision) rather than the specified precision, or bizarrely treating floating point arithmetic as commutative and associative. That bit of behavior isn’t specific to floating point - c compilers routinely optimize out security checks because they taken “undefined” to mean “free to change clear intent”.

C doesn't require IEEE754. It's strongly suggested as normative, Appendix F. But not required; after all, C17/18 still does not require two's complement for signed integers. C's model allows for very weird underlying machines.

x86 is extremely popular and 80 bit long doubles using the old FPU model can introduce weirdness on that platform. And yes, reducing the 80 bit long doubles to 64 bits after every operation significantly impacts performance. Or used to.

While f80 had some quirks it is an ieee754 floating point unit, and has all the ieee precision and semantics.

But my point was that saying floating point was somehow a magical non-deterministic beast is incorrect, and saying == shouldn't be allowed because of that is incorrect. Pretty much the same arguments can be made for integer arithmetic (after all x/y is undefined behavior if it overflows or y is zero even for integers, and just as with floating point you need to understand the behavior of arithmetic)

That's a huge stretch. Integer arithmetic is perfectly portable. Defined operations have bit-exact defined results, reproducible on any platform.

In contrast, the result of basic floating point operations is not. You may live in a world were you use a single cpu type, and a single OS, and a single compiler, in which case, it might appear to you as defined. But anyone involved in some portability across multiple systems know how improbable it is to rely on floating point for anything exact. Actually, don't. ever.

For anything which requires exact results, use integer instead.

No floating point is exact and consistent and incredibly well defined. Floating point is not exact but neither are integers. Floating point is 100% deterministic.

Things that differ: bit wise representation for non-ieee formats. Things that don’t differ: numeric result.

Basically for any arithmetic operation, with a given significand and exponent size in some rounding mode, there is a single correct result. Different implementations do not produce different results.