3 ms·
Generally, undefined behavior removes the need for systematically checking for special cases, the most common being out of bounds access. But it can go further
by GuB-42 2y ago
Generally, undefined behavior removes the need for systematically checking for special cases, the most common being out of bounds access.
But it can go further than that. Dereferencing a NULL pointer is undefined behavior, so if a pointer is dereferenced, it can be assumed by the compiler not to be NULL and the code can be optimized. For example:
void foo(int *p) {
*p++;
if (p == NULL) {
printf("val is NULL\n");
} else {
printf("val is %d\n", *p);
}
}
can be optimized to:
void foo(int *p) {
*p++;
printf("val is %d\n", *p);
}
Note that static analyzers will most likely issue a warning here as such a trivial case is most likely a mistake. But the check for NULL may be part of an inline function that is used in many places, and thanks to the undefined behavior, the code that handles the NULL case will only be generated when relevant. The problem, of course, is that it assumes that the programmer knows what he is doing and doesn't make mistakes.
In the case of memcpy(NULL, NULL, 0), there probably isn't much to gain making it undefined. It most likely doesn't help with the memcpy implementation (len=0 is a generally no-op), and inference based on the fact that the arguments can't be NULL is more likely to screw the programmer up than to improve performance.
- high_na_euv 2y agoBut how much actual performance is gained here?
- bagels 2y agoIt all adds up. All those instructions you don't have to execute, especially memory access and cache misses from jumps, pipeline stalls from conditionals, not just from this optimization.
- ncruces 2y agoImagine that you created a function GetPixel that reads an RGB pixel at a memory address, and which has a NULL check as a precondition. If the compiler can "prove" that the pointer is not NULL it can (after inlining the call) remove 20 million checks for a 20 megapixel image. The silly issue is the compiler using "you accessed it before" (aka "undefined behaviour") to "prove" that the pointer is not NULL. But I can attest that avoiding 20 million such checks does indeed make a huge difference.
- Dylan16807 2y agoIf we're inlining the call, then we can hoist the NULL check out of the loop. Now it's 1 check per 20 million operations. There's no need to eliminate it or have UB at that point.
- cv5005 2y agoJust make a non null checking version: GetPixelUnsafe() and let the responsibility onto the user to do the null check before the loop. All of these 'problems' have simple and straigtforward workarounds, I'm not convinced these UB are needed at all.
- ncruces 2y agoThat's a non solution for existing code that already calls GetPixel 20 million times. It's not like I'm saying C is the best possible way to write new code. I'm just commenting why this matters for performance, and “remove all undefined behavior" from C compilers is a non-starter. Now go write Rust for all I care.
- nemothekid 2y ago>All of these 'problems' have simple and straigtforward workarounds, I'm not convinced these UB are needed at all. He gave you a simple and straightforward example, but that example may not be representative of a real world program where complex analysis leads to better performing code. As a programmer, its far easier to just insert bounds checks everywhere, and trust the system to remove them when possible. This is what Rust does, and it safe. The problem isn't the compiler, the problem is the standard. More broadly, the standard wasn't written with optimizing compilers in mind.
- menaerus 2y agoIt depends on your CPU microarchitectural details, on the complexity and size of your binary executable and the workload of your binary. So there's no universal answer to your question but it could very well be "much".