61 comments

[ 3.4 ms ] story [ 86.7 ms ] thread
Nice summary.

If stability/security is important in your codebase. It's of course worth avoiding undefined behavior, and compiling with warnings as errors (-Werror in gcc).

However it's also really worth running your test suite through Valgrind [1] too sometimes. This instruments your code to check for use for use of initialized variables/other non-deterministic behavior at runtime.

I used to run this under continuous integration, and flag builds with issues.

[1] http://valgrind.org/

I agree that all warnings should be generally be turned on and snuffed out (Though there are a few innocuous warnings that I turn off sometimes), however I wouldn't recommend the usage of -Werror to force that unless it's only enabled for dev builds. Warnings are generally impossible to fully predict: different compilers can give different warnings - from warnings enabled by default, warnings changed between versions, and warnings that differ between compilers. And of course, there's no wrong way for the compiler to do it because what to warn about is subjective in the first place.

That can become really annoying for an end user looking to just compile and run your software, not debug why it fails to build because their setup outputs a warning somewhere that may not even be an error. And turning off -Werror tends to then require delving into Makefiles (or whatever other build system they chose to use), and at that point most people are probably just not going to bother and call it broken.

Again, I'm not saying you shouldn't attempt to snuff out and find every warning that you can, but you shouldn't have your default build rely on compiling with no warnings. There's just too many variables uncounted for to expect your software to compile without warnings in every possible configuration.

I also agree 100% with running test suites through valgrind - I do that as well and it's a very good way to guard against memory leaks being introduced (Of course, good structure and design techniques are always the best defense, but there's nothing wrong with a good check that things are doing good).

Right, I think that's reasonable. Maybe just use it for debug or builds of test suites/CI builds.
Another reason to avoid -Werror is if you pull in third party code that you share your build options with. Even large stable open source projects regularly spew hundreds of warnings when you aggressively turn them on.

I'll generally build my own code with all warnings, but use as close as I can to the package maintainer's build settings for their code. Even then, you can get warnings because the third party developer doesn't use your exact compiler.

AFAICT valgrind is mostly obsolete; use ASAN+UBSAN+... instead.
When gcc or clang are an option.
Is the thread sanitizer caught up to where valgrind (helgrind) was for detecting data races? I know a while back it wasn't but things may have changed.
The problem with Valgrind is that not everyone is working on one of these platforms

http://valgrind.org/info/platforms.html

It's also not enough. A medium-large program of mine that passed Valgrind had been working for years when it started blowing up on newer compiler releases. Maybe the newer static undefined-behavior analyses would have made that less of an awful pain, if they'd been available then.
-Werror used to be the way to go, until compiler writers decided to add warnings for idiotic reasons.

Like 'confusing indentation' or 'unused static const variables'.

Seriously, these two are completely idiotic; why would the compiler decide that the fact I don't have a blank line 'might' be confusing? on whose criteria? Same with static const variables in C files. Lots of code is often #ifdef in/out, and lots of people would rather have a static const int than a #define!

The "confusing indentation" warning can be useful. For example, it would have flagged

  if ((err = SSLHashSHA1.update(&hashCtx, &signedParams)) != 0)

      goto fail;

      goto fail;
Does any compiler try to indicate the severity of each warning. For example, using the value from an uninitialized variable is labeled as somehow "worse" than code-quality warnings like indentation or bare assignments inside conditionals?
Really nice summary of undefined behavior. Just realized this is posted by Chris Lattner himself.
"You're holding it wrong", compiler-writer's version.

IIRC, the Linux kernel has a rule that as a kernel developer, you are not allowed to break userspace, even if you think userspace is wrong. This is an apologia for why compiler writers think they should not have to be held to similar standards.

The argument is always something along the lines that UB in the C standard means they get to do whatever they want, including such nonsense as eliminating entire loops because the last access is out of bounds or going down an if-else-branch other than what the if says.

That this argumentation is specious in the extreme is shown by the fact that C compilers worked fine (and with less code breakages) before there even was a C standard, so all behavior was "UB".

The unstated assumption is that the optimizations enabled by this are absolutely necessary. That overstates the importance of compiler optimizations in general, and certainly the importance of these particular optimizations. (see Proebsting's Law)[2].

There may be code-bases where these extra optimizations are beneficial or even essential, fine, let's add a -Ogoforit or -Oletsgocrazy flag.

Rebuttal here: What every compiler writer should know about undefined behavior or "Optimization" based on undefined behavior hurts performance[1]

Short summary is that this trend towards exploiting UB breaks a lot of code that has been working for decades, and the fixes required to bring them in-line with the new regime imposed in order to enable these new optimizations makes it slower than the old code without those optimization. Yay.

[1] http://www.complang.tuwien.ac.at/kps2015/proceedings/KPS_201...

[2] http://proebsting.cs.arizona.edu 'I may be best known for "Proebsting's Law", which asserts that compiler optimizations have yielded annual performance gains an order of magnitude worse than hardware performance gains. The law probably would have gone unnoticed had it not been for the protests by those receiving funds to do compiler optimization research.'

Others have gone down this path and realized it's not as easy as it sounds: http://blog.regehr.org/archives/1287
Yes, but it depends a lot what you consider "down this path". John's approach seemed to be to define the UB, and to me it looks like he found out why it was left as UB in the spec.

Not exploiting the UB for optimizations in ways that break lots of code is actually trivially easy. We have been doing it for 30+ years and we just have to stop adding new optimizations that break, and possibly revert some of the ones that are already in there.

> Not exploiting the UB for optimizations in ways that break lots of code is actually trivially easy

Here lies madness. What optimizations are not allowed exactly? Strict aliasing? Assuming dereferenced pointers are not null? Assuming all array accesses are always in bound? Assuming that integers don't wrap? Assuming that allocated memory is not accessed after a free? Reusing function frame slots of variables with non overlapping lifetimes? Not laying down local variables in the most obvious way? Caching and reordering non-volatile memory accesses? Accessing local variables of functions that have returned?

I'm sure you will say obviously not allowed to the first few and obviously allowed to the last few, but at some point all of these optimizations (and more) did break some code as some point.

> Here lies madness.

Nah. You're looking at it the wrong way. You are saying madness lies in going there. We don't have to "go there". We can simply not leave there. Not doing something is easy. In fact, it's my secret super-power. If you want, I'd be happy to teach you.

Compilers already have these optimizations. Doing nothing would be leaving those optimizations in.

If what you want is a straightforward translation of C into machine code, there is always -O0.

> Compilers already have these optimizations.

git co <previous-hash>

You're welcome!

You're the person who wants this. Maybe you should do that? Also make sure you're willing to give up support for newer language standards, CPU architectures, and countless bugfixes (even C++98 support was getting pretty noticeable bugfixes in GCC 10 years ago).
But the problem is that everyone really wants their builds to run with -O3 (which should, and is, -Ogoforit).

It's really common for bugs to only appear when compiling with optimization.

Reasonably, when those bugs appear, people go fix them.

My guess is in LLVM these optimization are turned off for debug builds. But who wants to be running debug builds all the time?

A lot of the time non-optimized builds are fine. Unfortunately C (or rather C implementations) advertise the opportunity to make your code "as fast as possible" assuming you haven't made any mistakes, and too many programmers think "of course I want my code to run as fast as possible, and of course I don't make mistakes".
I think it's partly that if you're not looking for speed improvements then C, as a language, begins to look less attractive.

There are certainly safer alternatives, but not faster ones.

I'd probably agree though, in most cases users don't need the fastest builds (most OS tools). It would be interesting actually to be able to enabled optimized versions of functions as a runtime flag. But I've never seen that done.

This is one of the reasons I insist on my C/C++ code compiling with "-Wall -Wextra -Werror" and a few other switches.
No, there needs to be a article on -Wall -Wextra and -Werror being harmful, these are metaflags that:

1. Vary between compiler versions.

2. Vary between compiler implementations.

3. Often contain somewhat useless warnings (Wparentheses) that can change with changes in 1. or 2.

Why would anyone who didn't want these optimizations be using C in this day and age? Your link briefly mentions Rust, D and Go, but only to say the author hasn't really looked at them, when they seem to be exactly the solution the author wants.
Here's why: C is dang near as close to the hardware as you can get without touching it, it runs everywhere, and it has so little abstraction that it's hard to incur unexpected costs. Rust is closer to C++ here, and the other languages on your list are so far outside the ballpark that they can't even touch the edge of it.

It's not the only language of this sort, but C also has a lot of code written in it, which is a major advantage.

> C is dang near as close to the hardware as you can get without touching it

Not really true in the context of the optimizations done by modern C compilers (e.g. autovectorization).

In some ways. But, as I discussed (dragonwriter brought it up) in a cousin comment, C still does direct hardware access better than most other 3GLs. And that matters sometimes.
> C is dang near as close to the hardware as you can get without touching it

Not really; it's structurally nothing like assembly, which is as close as you can get to the hardware without just handcoding machine code.

Now, C -- for a structured language with static typing -- provides very little in terms of safety features that get in the way of direct hardware manipulation, whereas lots of languages with superficially similar structure aim to be safe in some ways by default which makes some hardware manipulation more difficult, or at least more verbose, than it would be in C. So, in some ways it gets less in the way of freedom to manipulate the hardware than some other structured programming languages.

But it's not particularly close to the hardware at all.

>Not really; it's structurally nothing like assembly, which is as close as you can get to the hardware without just handcoding machine code.

Here, assembly is touching the hardware. If you write your code in assembly, it's not portable to other architectures, even hypothetically.

>provides very little in terms of safety features that get in the way of direct hardware manipulation

Exactly. So it's a heck of a lot closer to the hardware than most 3GLs.

To support your argument, p-code is closer to what a portable, assembly language looks like. It's an abstract machine that can be trivially targeted to about any CISC or RISC machine. Let amateurs without compiler knowledge port Pascal/P to around 70 architectures in 2 years.

https://en.wikipedia.org/wiki/P-code_machine

Linoleum was another:

http://anynowhere.com/bb/layout/html/frameset.html

The closest in C land was C-- by Jones et al which still looks heavy compared to p-code:

http://www.cs.tufts.edu/~nr/c--/index.html

C itself isn't that close. Vendor extensions and relying on the compiler to pattern match certain code are needed to get access to actual hardware features, like, for instance, rotating the bits of an integer, vector instructions and efficient integer overflow detection.
Are these all undefined rather than merely unspecified or implementation-defined? I'm a little suspicious that they don't explain the difference, but I'm having trouble finding a definitive list of the places where the standard allows nasal demons.
They're undefined. (From a quick skim.)

However, an implementation could define various UBs however it wants. The difference between "undefined" and "implement-defined" is really that an implementation doesn't have to anything even remotely sensible for "undefined" behavior, but it has do define some ("sensible") behavior for "implementation-defined" behavior. Usually, "implementation-defined" behavior also tends to remain consistent across versions whereas "undefined" behavior usually has no such constraints. For example, for "undefined" behavior, version 5.1.2.3 of the compiler may differ drastically from 5.1.2.4 in terms of optimizations which rely on UB.

Basically:

Undefined - the compiler may assume this can never happen, and if it does happen, it can "poison" the whole execution including whatever logically happens before the undefined part.

Unspecified - the behavior must be deterministic and cannot affect the semantics of other well-defined parts of the program, but the implementation need not guarantee any single behavior.

Implementation-specified - like above, but the implementation must specify the exact behavior.

The compiler may also assume undefined behaviour can happen, and do something documented, that programmers would probably expect.

Even though the standard imposes no specific requirements, it explicitly calls that out at something you could do: http://port70.net/~nsz/c/c11/n1570.html#3.4.3

But for some reason nobody seems to ever think this is an option.

It wouldn't improve benchmark performance, which is what C compiler writers care about. And at this point in time any programmer who wants that kind of behaviour has probably moved on from C.
Well, that and undefined behavior sometimes would require absurd amounts of runtime checking (eg. on signed bit shifts, etc.).
Since I didn't get the reference, here's the explanation:

http://catb.org/jargon/html/N/nasal-demons.html

I love the mental image, but I think that description masks why this happening.

The compiler doesn't specifically search for undefined behavior and use it as an excuse to do something "quirky." Instead, it assumes the code is well-defined and interprets (and transforms) it in ways that preserve its semantics. For example, "One cannot dereference null pointers. The author dereferenced the pointer p. Therefore, p is not null. Since p is not null, the compiler can avoid checking it after the first dereference." The compiler is essentially assuming that the author knows something that it may not and just runs with it.

However, there are a lot of transformations--and corner cases--many of which can interact. This can produce complex unwanted behavior that looks like "nasal demons", but largely as a side effect. The compiler does not say "OVERFLOW DETECTED. REPLACING ENTIRE FUNCTION WITH NOOP"

//int dead = *P; // deleted by the optimizer.

If a compiler does that, it is broken. Dereferencing P clearly has side effects (i.e. segfault if P does not point into valid memory). A compiler must prove that its optimisation does not affect the program execution. This is clearly a change that does. It cannot optimize away operations with side effects. And if this is a valid optimisation according to the standard, then the standard is broken.

EDIT: If segfaulting is not a part of C, but a part of the hardware architecture, then the compiler shouldn't assume that a dereferenced pointer is not NULL. Either way, the compiler should behave in a way to avoid the least expected outcomes.

Well, by your standard most compilers used today are "broken". Possibly causing a segfault is not a side effect the compiler is required to respect.

If you want to force the compiler to not remove a memory operation in C, you need to define the pointer as "volatile"

Does this actually count as a side effect?

I didn't think that dereferencing a variable could ever be the sole cause of a side effect. I thought that side effects were only relevant in the context of a store (except for maybe volatile variables in which there are some ordering/barrier constraints applied as well). I realize that in practice, deferencing NULL can definitely cause a crash, but I don't know if it counts as a "side effect" in the typical "C" sense of the phrase.

I know that modifying the value of a variable which is accessed by means of a reference passed as an argument could definitely have side effects (since another unrelated function might read that data) and thus a store can’t be completely optimized away (but still can be reordered), but I don’t see why “dead” can’t be optimized away in this case.

In this case “dead” is a local variable, thus it can never be accessed by anyone outside of the function. Therefore, if there are no reads of the variable after that single store, I don’t see why the store can’t be completely eliminated.

In fact, this is a common issue when dealing with microcontrollers where configuration registers are sometimes written by assigning a value to a dereferenced address constant. That's why they get casted as volatile.

If your dereference compiles into a read from a register, then in the case that the register is clear-on-read like some interrupt status registers I've seen, the dereference will have unintended side effects.
Agreed. However, I believe that in C, standard variable types are assumed to not have such side effects on a read, which is why such variables must be explicitly declared as "volatile".
Segfaults are an implementation detail of your hardware architecture. They are not a "thing" in the C standard. In the good old days before protected mode on x86 was a thing (if I recall correctly windows 95 was from this era) most of the time you would not segfault (unless you addressed RAM that wasn't there? I'm not sure anymore).

If the C standard did not allow pointer dereferences to be optimized out, many optimizations would become impossible, and people would switch to a language that did allow this.

if P does not point to valid memory, then dereferencing it is UB. Therefore, the compiler is at liberty to do whatever it wants (whether or not that is a good idea is irrelevant for the purposes of this consideration).

If the compiler knows that this is undefined, then you have already broken your end of the deal and therefore the compiler is free to break their end of the deal.

Therefore, eliminating the line is completely okay and the only think broken there is the code.

> if P does not point to valid memory, then dereferencing it is UB

IIRC, simply making P point to anything other than an object or 1 past the end of an array is already UB. I suspect that this is because some architectures have dedicated address registers and can take an exception when an invalid address is loaded/calculated instead of doing it when the memory access happens.

> IIRC, simply making P point to anything other than an object or 1 past the end of an array is already UB.

I don't think this is true in the strict sense you're describing it. Certainly forming a null pointer isn't UB as I understand it, only dereferencing one.

Yes, NULL is another exception that I forgot. I'm not sure this is ever explicitly stated as a distinct rule in the standard, but it seems to be implied by various arithmetic and conversion operations on pointers being UB if the result doesn't point to a valid object.
The standard imposes no requirements on how an implementation behaves with a program that does that. While I would agree that that makes C a not-very-useful language for most modern applications programming, what requirements would you have the standard impose? On some architectures reading * P might trap/segfault, so you cannot require that this line be harmless. Other architectures might read a value into dead (e.g. embedded systems with no MMU/segmentation) and continue harmlessly, so you cannot require that the line have some effect. If you make a requirement that if the line segfaults that counts as an externally visible effect then you basically disallow the compiler from reordering memory accesses at all, which could cost a lot of performance. Java shows it's more-or-less possible to achieve close-to-C performance while imposing consistent requirements on null dereferences, but only with the help of a highly-tuned JIT that consumes quite a bit of memory and startup time, which could be prohibitive for many environments.
Two things:

1. What's the proper way to do a NULL check? In the first example, they show how a NULL check could be optimized away, but I didn't see where they explained how to do it the right way. I might have just missed it though.

2. It would be nice if compilers offered a special optimization flag that performs most optimizations, but also “fills in the gaps” for non standard behavior by behaving how people expect. I realize that this would essentially be like each compiler implementing its own version of the standard in a way, but the reality is that if 99% of the people are making an assumption about how the compiler is going to do something, maybe that’s how the compiler should do it? Perhaps like a “speed+security” optimization flag. GCC already has some flags to handle certain questionable things such as strict aliasing, etc… Maybe something already exists, IDK. I guess the real solution would be to change the actual standard.

EDIT: A comment elsewhere provides a good response to my second point. It was along the lines of "which optimizations are okay then? Most optimizations broke code at some point".

I guess my response would be to only disable the ones that are the most common trouble makers. Another factor could be the performance gain vs amount of bugs it causes. I am reconsidering my second point though...

To 1. I think if you were to do the null check and then dereference, the problem would go away. Oh - and use the variable holding the dereferenced data.
> 1. What's the proper way to do a NULL check? In the first example, they show how a NULL check could be optimized away, but I didn't see where they explained how to do it the right way. I might have just missed it though.

You need to not do * p at all if p is NULL (i.e. move the "int dead = * p" after the early return).

If the pointer is null and you go ahead and dereference it anyway then yes, that's dumb, and the compiler is free to remove the null check.
Let's tell it like it is, the only reason C was more popular than anything else in the 70/80's is because unlike its competition, implementing a C compiler didn't require to pay a big license fee to a vendor. C is a horrible language and now most developers are stuck with it, because the Linux kernel like most system is built with C. Rust might save us from C ultimately, but that's clearly not for tomorrow.
Let's try this again. As a language C is not that horrible and it doesn't have to be as sloppy and as insecure as it is in popular compilers. In fact, I believe 99% of the C code is not performance critical and can benefit from simple runtime checks to ensure memory safety. Not that much needed to "save us from C". But ultimately no one cares, inventing and playing with new languages is way more fun.