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.
> Dead code removal is unexpected by the programmer, because programmer does not write dead code.
Of course it is expected, because the optimizations leading to dead code are fundamental for the performance you get out of a release-mode binary (and that's also true for languages with less UB like Rust btw).
When you have a function:
static int add(int a, int b) {
return a + b;
}
And you call that function with const parameters: int c = add(2, 3);
Then you want that entire function call to be removed and "folded" into its result 5, and when this was the only place the function was called, you'd also want the actual function to be removed from the binary (because what's the point of lugging code around in the binary that's guaranteed to never be executed).Here's example from article:
static int f(int a, int b)
{
x = b ? 42 : 43;
return a/b;
}
Compiler can inline `f` but if it removes conditional branch, that's where issue is. I wrote this branch because I expected `b == 0` to be a valid value. Compiler deduced that it can't be a valid value and decided to remove `43`. That's understandable. But let me know, so I'd rewrite my code myself instead. Because I have assumptions and compiler have assumptions and these assumptions do not match. It means that there's a bug.Because as far as it’s concerned that is the same thing.
Both clang-tidy and sonar will flag this since it’s broken on its own, but when you’ve fixed it you want the compiler to warn you every time this is inlined in a function where b is known non-zero and the check is redundant?
As flo told you, if you want a non-optimizing compiler use a non-optimizing compiler.
Dead code elimination is like the single most profitable optimization in the compiler, especially in conjunction with inlining and constant propagation that makes several branches become trivially dead. And its removal is very much expected by programmers, because (for example) bounds checking tends to work by generating bounds checks for every possible array access and relying on the compiler to be able to prove most of those bounds checks redundant.
No, my suspicion is you're actually only against certain kinds of dead code removal, but the kinds you're against are so inconsistent that it's impossible to implement a decision program except by including a routine to read the programmer's mind.
DWIM is not a spec.
Usually it means GC, bound checking and no pointer forging.
Plenty of literature on the matter, some retro computing fun, or using some of the survivors in modern platforms, for example Ada.
void clear_buf(char *buf, size_t len) {
char *end = &buf[len];
if (end < buf) return; /* in case len is so long the pointer wraps the memory space */
while (buf < end) *buf++ = 0;
}
(which it can do, by virtue of saying that constructing a pointer beyond end of buf is itself undefined behaviour, therefore it can expect that end is never less than buf. the more appropriate way to write this is "while (--len > 0) *buf++ = 0" and let the compiler invisibly change that to end=buf+len if it thinks that'll work in all situations and be more efficient than counting down len while moving along buf)...but you wouldn't be upset if it changed this:
for (int i = 0; i < 1000000; i++) {
if (i < 5) foo(i);
/*else something_else(i);*/
}
to effectively this: for (int i = 0; i < 5; i++) {
foo(i);
}
or even: foo(0); foo(1); foo(2); foo(3); foo(4); int main() {
int x = 0;
return g(x);
}
int g(int x) {
if (x == 0) return 1;
return bar();
}
int bar() {
...
}
Can this program be simplified to just int main() {
return 1;
}
Simple inlining, constant propagation, and dead code elimination gets you there.