Remix.run Logo
▲ vbezhenar 2 hours ago

My main issue with UB in C is that it's silent.

I'm OK with compiler doing wild thing, OK, whatever. Well, I'm not OK but I can accept it in this crazy world.

But I want to have loud warnings! Like WARNING: this conditional operator has been collapsed to one branch because earlier division by zero is UB. And now I can notice it and rewrite it or just remove that condition.

I understand that this code can be result of macro expansion. That's OK. Macros should either include some pragmas to temporary disable specific diagnostics or user should surround macro usage with these pragmas, if they can't edit the macro. It's already happening with other warnings.

Or maybe compiler could be smart enough to distinguish macro expansion from honest user mistake, I don't know.

I remember when C++ compiler just removed function epilogue where I wrote simple infinite loop. That was so crazy. So instead of entering the infinite loop, my program just continued to execute the function that happened to be linked below. Imagine debugging that. Zero diagnostics.

▲jakobnissen 2 hours ago | parent | next [-]

This is completely infeasible. A C compiler makes so many assumptions that UB does not occur that every program, even well functioning ones, would emit walls of warnings. Also, these warnings can't be silenced. Consider the integer division case: Should the warning be emitted every time division by zero is encountered at runtime? In that case, C would be much, much slower. Or should the compiler emit the warning whenever it can't prove at compile time that the divisor is not zero? In that case, you would get tonnes of situations where you can't fix the issue because you can't prove to the compiler it's not zero. Division by zero is a easy case. It gets much harder to emit warnings for aliasing assumptions, for example.

▲vbezhenar an hour ago | parent | next [-]

The compiler should emit a warning at compilation time, when it decided to replace the conditional with single branch, throwing away the condition itself and the second branch.

If I wrote some code, I expect it to be present in the binary. I don't just write code to be removed by the compiler. If that expectation was wrong, compiler should inform me about that.

▲quietbritishjim 36 minutes ago | parent | next [-]

How about if the compiler translates a pointer deference into a read of that memory address? That is potential undefined behaviour that has been "optimised" into something simpler than a safe operation (tracking all memory allocations and checking if the pointer is correctly pointing into one of them). So every pointer deference would also generate a warning (except perhaps where the compiler can prove from local information that it's safe).

It is definitely a hopeless path.

▲flohofwoe 38 minutes ago | parent | prev | next [-]

> If I wrote some code, I expect it to be present in the binary.

That's oversimplified. If after inlining and constant folding an if-condition turns out to be always true or false I would definitely expect that the compiler removes the dead branch.

This type of optimization is the base for the fabled "zero-cost-abstraction" (which isn't only a C++ thing, C code depends on it just as much), and removing those optimization would seriously tank peformance in any non-trivial codebase.

▲cesarb an hour ago | parent | prev [-]

> If I wrote some code, I expect it to be present in the binary. I don't just write code to be removed by the compiler.

It's very common to write code in templates or inline functions expecting the compiler to remove it if it's not relevant on the calling site. That's part of what makes "zero cost abstractions" have zero cost at runtime.

For instance, I have a SIMD routine with extra code to process the tail (leftover elements smaller than the native vector size). When the compiler can prove that the size of the input will always be a multiple of the vector size (which is very common for my use cases), it will completely remove that tail handling code.

▲vbezhenar 23 minutes ago | parent [-]

They you'll see that warning and mark your extra code to remove that warning, because you're aware that it's subject to potential elimination.

▲bigstrat2003 an hour ago | parent | prev [-]

> A C compiler makes so many assumptions that UB does not occur that every program, even well functioning ones, would emit walls of warnings.

The problem here is that the C compiler ever assumes that UB doesn't happen. That is empirically very much not the case, therefore the compiler should never be allowed to assume a lack of UB unless it can somehow prove that to be true. I honestly don't really care how many optimizations that would break; correctness is king. Software that goes fast is only worthwhile if it works correctly.

▲dgrunwald 19 minutes ago | parent [-]

> I honestly don't really care how many optimizations that would break; correctness is king.

It's approximately all optimizations. Good news: there's already a compiler option that does exactly what you want: -O0. It's even enabled by default (unless overridden by another -O switch)!

▲ahartmetz 7 minutes ago | parent [-]

-O0 code also omits a lot of "Don't be very stupid about it" optimizations, it's not a realistic option for much production code.

▲flohofwoe an hour ago | parent | prev | next [-]

First we need to reduce the UB zoo because the types of UB in the C standard go all over the place (from "no newline at end of file" to "oops, this specific UB combined with this specific code compiled on this specific version of this specific compiler with these specific compile options leaks execution into the next function").

Also AFAIK the point where UB causes 'runtime disruption' is way after the C frontend in the optimizer passes, e.g. much too late for issuing compilation warnings even if the UB situation could be detected (because as far as I understand the problem, the breakage happens mainly because of unexpected 'spooky actions at a distance' between different optimizer passes, e.g. a specific optimizer pass doesn't even notice that it broke the code).

You can get runtime errors for a lot of serious UB problems via UBSAN though of course (at the cost of some performance).

▲masklinn 2 hours ago | parent | prev | next [-]

Most UBs are runtime conditions, and inserting runtime checks would defeat the point of optimizing for them not possibly happening.

For the static ones there are often warnings you can set, but you’ll have to go through the list. Or possibly external checkers (e.g. clang-tidy has one for infinite loops but not sure it’s 1:1 with the optimiser on complex cases)

▲vbezhenar an hour ago | parent [-]

It's not about inserting checks. It's about removing code that I wrote from the binary. That should not happen silently.

▲flohofwoe 17 minutes ago | parent [-]

You can get that behaviour already today by simply not enabling optimizations.

The resulting performance difference is basically the price to pay for such a 'strict' compiler which translates the input source code straight into machine code instructions without attempting to simplify the output code via inlining, constant folding and dead code removal.

▲vbezhenar 7 minutes ago | parent [-]

-O0 program is useless and should be written with any other language. The whole point of C is to use compiler with optimizations. I'm not against optimizations. I'm against optimizations that perform things unexpected by the programmer. Dead code removal is unexpected by the programmer, because programmer does not write dead code.

▲rramadass 2 hours ago | parent | prev [-]

Relevant:

Memory error checking in C and C++: Comparing Sanitizers and Valgrind (quite comprehensive) - https://developers.redhat.com/blog/2021/05/05/memory-error-c...