The thing is, the redundant null check usually doesn’t appear literally in the source, but comes from an inlined function call (or even a macro). In that case, the null check is probably not a mistake; it’s just that this particular caller of the function that got inlined never passes null, and so will never trigger that code path.
I do think there’s unexplored potential for compilers to identify when they’ve eliminated code that didn’t come from inlining, and warn the user about the potential bug. However, this is much easier said than done, since the IR has gone through a lot of transformations by that point and there are a lot of edge cases to worry about. For example, even if all the IR instructions corresponding to a given piece of source code are dead, that code might still have affected the function compilation in some manner and thus isn’t useless – such as by being merged with a different (or perhaps redundant) computation, or by affecting control flow.
Also, even if some code path really is useless as compiled, it might be for innocuous reasons. For example, perhaps a given null check was not itself inlined, but the value being checked comes from an inlined function:
void *get_a_pointer(void) {
static int x;
return &x; // not null!
}
int main(void) {
void *p = get_a_pointer();
if (p == NULL) …
}
Unlike in the reverse situation where an inlined null check might be useful to another caller, here we know that that null check will never fire – that is, in this program, in this configuration, the check is useless. But perhaps with a different set of configuration flags, or on a different platform, the code uses a different implementation of get_a_pointer() which could return null. Or perhaps there is an abstraction boundary between main and get_a_pointer, where the documented interface of get_a_pointer says it might return null: even if the current implementation never does, the author expects that that might change in a future version of the code, so clients should proactively include a null check to avoid future breakage. (Of course, get_a_pointer might be part of a third-party library, and the author of main might have never even peeped at its implementation. These situations are especially common if you’re using LTO.)Something that's actually a pointer offset, not a dereference, like `int *n = ¶m->length`, is also an assertion of being non-null.
It seems reasonable to amend the C standard to say "That's not an assertion of being non-null" (and more generally to make &invalid_pointer->item and &invalid_pointer[n] valid; IIRC implementing offsetof as (size_t)(((type *)NULL)->member) is UB). Would that prevent any significant optimizations?
Also does the rule that pointers can only point to elements of allocated objects or one-past-the-end enable any significant optimizations?
Yes, for segmented architectures like the 8086, it means that pointers don't have to be normalized after every operation. That is, when you increment or decrement a pointer, you only need to do the operation on the offset, you don't have to touch the segment. When all you have is less than 10 MHz, every operation counts.
(For those who don't remember the 8086, it's not as simple as concatenating the segment and the offset to obtain the physical address: to get the 20-bit physical address from the 16-bit segment and the 16-bit offset, the formula is segment*16+offset. As a consequence, the same memory location could be accessed through several distinct segment:offset pairs. If the memory allocator chose the segment value right, objects of 64 KiB or less could be indexed by doing all operations on the offset value, without having to touch the segment value.)
I agree, it would be best if the compiler rejected this as clearly incorrect code. I think that needs a change in the spec relating to undefined behavior, though (and more importantly a change in direction from compiler vendors).
Edit to add: your second example, type aliasing, is a good one too. I think that one could also be usefully addressed by a compiler that flags it as a likely error. Although you’d need some way to annotate the code in the event that the weird aliasing behavior is actually correct and expected (I dunno, make “len” volatile or something?)
My concern is largely about how to phrase "The compiler may not delete this dead code" in a way that's meaningful. For instance, I assume the intent of the proposal is not to forbid optimizing this code in the obvious way, right?
int n = param->length, i;
if (param == NULL) {
return;
}
if (param == NULL) {
return;
}
for (i = 0; i < n; i++) {
do_something(param->data[i]);
}
What about this? int n = param->length, i;
for (i = 0; i < n; i++) {
if (param == NULL) {
return;
}
do_something(param->data[i]);
}
which is more likely to show up in the form of an inline function that checks for NULL - can the compiler refactor that check into a single one?> I think that one could also be usefully addressed by a compiler that flags it as a likely error.
The compiler can't know when compiling that one function whether src->len and dest->data alias. It might be compiling that file as a standalone object file, with no other context about who's calling it. Should it emit a warning when compiling that code? (What's the warning, "I'd like to optimize this but I can't?")
Alternatively, should it flag the function ... somehow ... so that when someone else calls it with aliasing arguments, that's an error? That isn't even a capability available to the C language as written: it's valid to call that function knowing nothing other than the struct definition and the function's prototype.
You can imagine a language where variables carried information about whether they alias. (Actually you don't really have to imagine; the `restrict` keyword does a subset of this in C itself, and other languages have much more explicit ways of tracking this, such as Rust's shared vs. mutable references.) But it would be hard to retrofit such a thing onto existing C code.
The fundamental problem with the C language working group is that they're obligated to keep working on C as it already exists.
If so, then again, this has the fundamental problem that you're expecting the compiler to implement some optimizations - like pushing the dereference out of the buggy driver via LTO - and not others. If the compiler had all optimizations off, it would just generate code to read from x->buffer in the obvious place, which would page-fault at runtime before the check from core code even happened.
If the core code implements the check first, then this optimization won't cause you problems, because this optimization is strictly about checking for a NULL pointer after using it.
For the specific case of computing the address of x->buffer without actually dereferencing it, I do agree that the compiler should not consider that to be an implicit non-null promise. That seems much more likely to be a change you can make without breaking the language spec. See the subthread with 'dbaupp.
(Alternatively, you could work on building tools for driver writers to write in a less error-prone language than C, because drivers being of poor quality is a much broader problem with much broader impacts than just triggering ill-advised optimizations under LTO, and simply optimizing drivers less efficiently isn't going to solve the problem that the driver shouldn't be dereferencing x->buffer without its own NULL check in the first place. If you check my recent GitHub activity you might notice a project to do exactly this in Linux....)
I am expecting the compiler to only make optimizations based on things it knows are true - that are true. So if the source code is "check for null;check for null; do something;" the compiler should be able to delete the second check (hopefully with a warning). In fact, the Standard is very explicit about permitting optimizing compilers to delete repeated operations. However, in the case I described, the compiler does not know that the pointer is non-null, it's just easier to assume it is not null. That leads to an "optimization" that is not an optimization. I agree that requiring compilers to only use true optimizations is more of a challenge than allowing them to use false ones, but nobody said it was going to be easy. In particular, it looks like Clang loses way too much information going from source to llvm language which makes it difficult to carry information about whether, for example, a pointer dereference is safe or not - but that's a defect in the compiler, not in the source language.
Yes, that's exactly what I had in mind! :)
More generally, it might be the case that enough of the last several years of unpopular optimizations are controlled by flags that you can compile the code twice, once with the optimization enabled and once without, and warn whenever the optimization changes something. It might even be possible to do this with a shell script that treats the compiler as a black box with -ffunction-sections and uses objdump or something.
I do wonder how hard it is to write code that produces the same object code regardless of something more major like -ftrapv, though.
The optimization isn't written in the form of "If you see this ridiculous code, make it less ridiculous." It's written in the form of "You can refactor repeated checks of presumed-non-aliased variables into a single check." Otherwise code like
for (i = 0; i < *plen; i++)
would involve dereferencing plen on every single loop, which for many applications is definitely not marginal!> some buggy code dereferences a null pointer - which actually works in this implementation.
What implementation is this? (Are you mmapping at NULL, or writing for an architecture without an MMU, or something?)
NULL doesn't have to be address zero. It's denoted by literal zero, in the C source, but it can be something totally different when translated to the machine. Can you arrange for some other address to generate a segfault, and tell your C compiler to use that as NULL? (I don't know how to do this with gcc, and I'd like to know how, honestly - it seems like a significant win for kernel security if NULL pointers in kernel code were an invalid kernelspace address and not an invalid userspace address.)
If you really think C is enough of a disaster that this is justified, why even care about fixing C?
Here's the thing I don't quite understand: I gather that this is advocating for something like GCC's -fno-delete-null-pointer-checks to be part of the spec, but, how is -fno-delete-null-pointer-checks actually helpful?
I think it's because if you write code like this:
with -fdelete-null-pointer-checks the compiler might optimize it in two successive steps to delete the check (because param->length was already dereferenced) and then move the dereference down to the beginning of the for loop, and with -fno-delete-null-pointer-checks the compiler might skip the first optimization and keep the second.But for -fno-delete-null-pointer-checks, you're relying on the second optimization, moving the dereference later. Otherwise you'd just have segfaulted already (or, in a kernel context, the read might have succeeded from a userspace page mapped at the null page). Right?
It seems reasonable for the compiler (or some other tool) to produce a hard error here and prevent this code from compiling, saying, hey, this is clearly doing something unwanted. It doesn't seem reasonable to expect the compiler to do certain optimizations to make your code safe, without which your code would have a security vulnerability, and also get mad at the compiler for doing more optimizations that return it to the original state of things.
> The limitation of lvalue access by types has never made much sense and required a undefensible exception for character pointers.
My understanding of type-based alias analysis is that it's required for code like this to optimize the way you'd want:
Otherwise the compiler would have to worry that dest->data would overlap with src->len and re-read it every time!You can do this without type-based alias analysis in a language that gave you different tools for managing variables, but doing so backwards-compatibly in C seems difficult, and this proposed rule does not seem to help.