9

Recently I ran the following code on ideone.com (gcc-4.3.4)

#include <stddef.h>
#include <stdio.h>
#include <stdlib.h>
#include <new>

using namespace std;

void* operator new( size_t size ) throw(std::bad_alloc)
{
     void* ptr = malloc( 2 * 1024 * 1024 * 1024);
     printf( "%p\n", ptr );
     return ptr;
}

void operator delete( void* ptr )
{
    free( ptr );
}

int main()
{
    char* ptr = new char;
    if( ptr == 0 ) {
        printf( "unreachable\n" );
    }
    delete ptr;
}

and got this output:

(nil)
unreachable

although new should never return a null pointer and so the caller can count on that and the compiler could have eliminated the ptr == 0 check and treat dependent code as unreachable.

Why would the compiler not eliminate that code? Is it just a missed optimization or is there some other reason for that?

sharptooth
  • 167,383
  • 100
  • 513
  • 979
  • 17
    What do you mean "`new` should never return a null pointer"? **You** wrote the `operator new()`! Clearly *your* version does happily return a null pointer. If you don't respect the rules of the standard, anything can happen. – Kerrek SB Nov 21 '11 at 14:48
  • 1
    Yeah, this question makes no sense. You're asking the compiler to perform an optimization that would actually result in incorrect code. Leave in the default implementation of `new` and then look at the assembly output to see if the compiler eliminates the dead code. – Omnifarious Nov 21 '11 at 14:52
  • @Kerren SB: I mean that the compiler should believe the Standard and just assume that `new` should in no event return null and optimize the check away and if my code breaks because my replacement returns null - that's my fault anyway. – sharptooth Nov 21 '11 at 14:59
  • 2
    @sharptooth: A `new` expression is more than a just an allocation. I don't think you can optimize over this entire chain of commands. See my answer for details. (It would be an entirely different question if you had said `void * p = ::operator new(1);`.) – Kerrek SB Nov 21 '11 at 15:02

8 Answers8

7

I think this is very simple and you have two fundamentally different things confused:

  1. malloc() can return anything, in particular zero.

  2. the global C++ allocation function void * operator new(size_t) throw(std::bad_alloc) is required by the standard to either return a pointer to the required amount of storage (+ suitably aligned), or otherwise exit through an exception.

If you want to replace the global allocation function, it is your responsibility to provide a replacement that abides by the rules of the standard. The simplest version looks like this:

void * operator new(size_t n) throw(std::bad_alloc) {
  void * const p = std::malloc(n);
  if (p == NULL) throw std::bad_alloc();
  return p;
}

Any serious implementation should actually contain a loop to call the registered new-handler until the allocation succeeds, and only throw once there are no more new-handlers.

The program that you wrote is simply ill-formed.


Digression: Why is this new defined that way? Consider the standard allocation sequence when you say T * p = ::new T();. It is equivalent to this:

void * addr = ::operator new(sizeof(T));  // allocation
T * p = ::new (addr) T();                 // construction

If the second line throws (i.e. construction fails), the memory is deallocated with the corresponding deallocation function. If the first call fails, though, then the execution must never reach the second line! The only way to achieve this is by exiting through an exception. (The no-throw versions of the allocation functions are only for manual use where the user code can inspect the result of the allocator before proceeding to construction.)

Kerrek SB
  • 464,522
  • 92
  • 875
  • 1,084
  • 1
    Okay, it's my responsibility to provide a well-formed replacement which I fail to do, but why would the compiler not count on me and optimize the check away? – sharptooth Nov 22 '11 at 06:23
  • @sharptooth: As I said in my other comment, the `new` expression may well be far too complicated to optimize out this one step half-way down. Also, the standard doesn't say explicitly that `new char;` will never be zero, only that `::operator new(1)`, the one with the `bad_alloc` exception-specification, will never return a null pointer. If at all I imagine you could try and see if *that* is optimized out... – Kerrek SB Nov 22 '11 at 12:48
  • I see your point. The behavior is the same with `::operator new()` being called explicitly. – sharptooth Nov 22 '11 at 12:56
  • Interesting. Well, the compiler isn't *required* to optimize anything :-) This is probably such an obscure niche situation that it's not worth bothering with. Any sane *user* code would never include the null check, so why add an optimization routine for bad code... maybe try a few other compilers, though. – Kerrek SB Nov 22 '11 at 13:00
5

C++11 is clear on the issue:

void* operator new(std::size_t size); : ... 3 Required behavior: Return a non-null pointer to suitably aligned storage (3.7.4), or else throw a bad_alloc exception. This requirement is binding on a replacement version of this function.

You hit Undefined Behavior.

[edit] Now, why would this impede optimization? Compiler vendors tend to spend their time dreaming up optimizations for code patterns that are commonly used. There's usually little benefit for them to optimize for faster Undefined Behavior. (Some UB may be well-defined on that particular compiler and still be optimized, but the above example likely wouldn't be).

MSalters
  • 173,980
  • 10
  • 155
  • 350
  • This doesn't prohibit the compiler from performing the optimization I was asking about in any way. – sharptooth Nov 22 '11 at 06:20
  • "There's usually little benefit for them to optimize for faster Undefined Behavior" -- But that's not what is being asked for; checking for null after new is not UB. Suppose the coding standard says to always check for NULL pointers, without exception (I have such a client). For them, the optimization is valuable. – Jim Balter Feb 25 '13 at 06:19
  • @JimBalter: Then fix the client. Given `int *p`, the postconditions of both `p = new int` and `if (p)` are that p is not null. If you'd need to check in the first case, you must logically also check in the second case. I.e. `if (p) { if (p) ...`. And since the latter is obviously redundant, so is the former. – MSalters Feb 25 '13 at 15:47
  • client = customer. I can't fix them. I'm well aware of your point about logical equivalence ... but it works against your argument: the compiler *does* optimize a null test of `p` after `if(p)`. – Jim Balter Feb 25 '13 at 17:42
  • @JimBalter: Ah, but that's in fact a common and occurance. It happens when the compiler inlines a function and both caller and callee have the same null pointer check. It's also a special case of eliminating the second execution of an idempotent check. – MSalters Feb 26 '13 at 18:42
  • @MSalters Yes, the optimization is justified by common occurrence ... I just riffed off your point about logical equivalence. Believe me, I know how absurd and inept is my client's "coding standard" that says unconditionally to "check pointers for null before using them" ... and doesn't even say what to do if so; generally they *hide the bug* by skipping code or *propagate the bug* by returning NULL from a function that shouldn't logically return NULL. They don't use assert because they don't want to crash the app. They don't log because ... even they don't want *that* much unnecessary code. – Jim Balter Feb 26 '13 at 22:34
  • The question isn't about optimizing for "faster UB", it's about using the fact that `ptr == 0` should always be false to make the fast-path faster. i.e. the normal strategy of optimizing based on the assumption of *no* UB. The fact that the question actually triggered UB instead of just looking at compiler-generated asm to see if there was a check or not is somewhat beside the point, unless the compiler was actually inlining custom `new` and then choosing to optimize or not based on that. https://godbolt.org/z/zfPazoo8K shows clang11 and later doing this optimization (when inlining `new`!) – Peter Cordes May 06 '22 at 15:19
4

I think you're expecting way too much of the optimizer. By the time the optimizer gets to this code, it considers new char to be just another function call whose return value is stored on the stack. So it doesn't see the if condition as deserving special treatment.

This is probably triggered by the fact that you overrode operator new, and it's beyond the optimizer's pay grade to look in there, see you called malloc, which can return NULL, and decide that this overridden version won't return NULL. malloc looks like Just Another Function Call. Who knows? You might be linking in your own version of that, too.

There are a couple other examples of overridden operators changing their behavior in C++: operator &&, operator ||, and operator ,. Each of these has a special behavior when not overridden, but behave like standard operators when overridden. For example, operator && will not even compute its right hand side at all if the left hand side evaluates as false. However, if overridden, both sides of the operator && are computed before passing them to operator &&; the short-circuit feature goes away completely. (This is done to support using operator overloading to define mini-languages in C++; for one example of this, see the Boost Spirit library.)

Mike DeSimone
  • 41,631
  • 10
  • 72
  • 96
  • *`malloc` looks like Just Another Function Call* - That's not true; compilers do already special case it so they know that freshly-malloced memory doesn't alias anything else, and that no global variables have pointers to that malloced memory. And they know its alignment. [GCC has an `__attribute__((malloc))`](https://stackoverflow.com/questions/18485447/gcc-attribute-malloc.) to decorate malloc-like function prototypes to provide this information. – Peter Cordes May 07 '22 at 11:47
  • Your reasoning about other overrides doesn't fully work: overriding `new` does *not* remove the requirement that it can't return `nullptr`. But overriding other operators like `&&` *is* allowed to change their semantics. It's certainly possible that a compiler would give up on seeing replacement of `new`, but the standard does provide enough guarantees that **the optimization being asked about would is legal. (And in fact recent clang does so**; see [my comments](https://stackoverflow.com/posts/comments/127469633) on another answer.) – Peter Cordes May 07 '22 at 11:49
  • Yeah, it’s amazing how a decade-old answer isn’t accurate any more. Did you know that malloc didn’t zero memory either in the old days? If you wanted that you had to call calloc, which had no C++ analog (because initialization is the constructor’s job). Special-casing malloc is one of many things added to improve security since this answer. As for operator new, if you’ve disabled exceptions, then returning nullptr is the only way it can tell you the allocation failed. And disabling exceptions is commonly done on constrained embedded platforms. – Mike DeSimone May 07 '22 at 21:31
  • Heck, back when I wrote this, compilers did not routinely inspect printf-like functions to make sure tokens and arguments agreed. That’s another one of the GCC attributes that didn’t exist back then (gcc 2.95.3 I think). – Mike DeSimone May 07 '22 at 21:33
  • No need to be that snarky about it. In Nov. 2011, [current GCC](https://gcc.gnu.org/releases.html) was 4.6.2. GCC2.95.3 was a decade earlier, 2001. Even GCC4.1 *does* know that two malloc results don't alias with each other, thanks to use of `__attribute__((__malloc__))` in its headers. (https://godbolt.org/z/b57KfKP1j shows 4.4 - comments explain what to look for in the asm, and you can see the change if the pointers are returned by an actual unknown function). GCC4.6 was well beyond the dark ages. And some `malloc` implementations can still return non-zeroed memory from the free list. – Peter Cordes May 08 '22 at 04:18
  • Interesting point about `-fno-exceptions`, though. Clang trunk still optimizes away the nullptr check even with `-fno-exceptions` - https://godbolt.org/z/94x3rePsz. That's arguably a compiler bug; it's a `new char` so there's no default constructor that would already dereference the pointer to initialize it. Or even get called with a possibly-NULL `this`. – Peter Cordes May 08 '22 at 04:28
  • I haven't messed with Clang on embedded targets at all, but I'm not sure using `-fno-exceptions` with it is advised. I'm kind of surprised it even allows it. And yes, 4.6.2 sounds right, but remember that the embedded community loves to require compilers that are out of date because they're "stable." For example, as of this comment, [GCC 12.1 is latest](https://gcc.gnu.org/releases.html), while [GCC Embedded for ARM is on 10.3](https://developer.arm.com/tools-and-software/open-source-software/developer-tools/gnu-toolchain/gnu-rm/downloads). – Mike DeSimone May 09 '22 at 05:09
  • (Another problem with `-fno-exceptions` is that compilers don't always provide no-exceptions versions of standard libraries, so you run into things like "integer divide checks for zero and tries to throw an exception even without exception support, so the program cannot link." Had to write my own division function for that.) – Mike DeSimone May 09 '22 at 05:13
  • That's fine, I just think this answer was too pessimistic about it being possible or even legal. Certainly compilers at the time didn't actually do the optimization, but the way you justify that makes some serious overstatements, even for 2011. (e.g. about `new` and `malloc` being just opaque functions for the optimizer). – Peter Cordes May 09 '22 at 08:08
  • More importantly, the operator overload argument seems off-base because *replacing* `operator new` doesn't relax the non-null requirement in the standard, unlike how *overloading* other operators removes their short-circuiting semantics. It's totally possible that a compiler might treat it differently when replaced, so that's a valid hypothesis, but the way you phrase it seems like you're claiming that ISO C++ says the behaviour changes when `operator new` is replaced. (At least that's how I read "other examples ... in C++".) That part is what made me decide to downvote. – Peter Cordes May 09 '22 at 08:09
3

Why should the compiler do so ?

With an opaque implementation of new it's impossible to know whether the implementation is correct or not. Yours is non-standard, so you are lucky that it did check after all.

Matthieu M.
  • 287,565
  • 48
  • 449
  • 722
2

There are more than one operator new; See here. And you did not declare your one as possiblity throwing an exception. So the compiler should not infer it does never return a null pointer.

I don't know very well the latest C++11 standard, but I guess that it is only the standard defined operator new(the one throwing exception) which is supposed to return a non-nil pointer, not any user defined ones.

And in the current GCC trunk, file libstdc++-v3/libsupc++/new don't seem to contain any specific attribute telling GCC that nil is never returned... even if I believe it is undefined behavior to get nil with a throwing new.

Basile Starynkevitch
  • 223,805
  • 18
  • 296
  • 547
  • Well, I tried to add `throw(std::bad_alloc)` - the behavior is the same. – sharptooth Nov 21 '11 at 14:35
  • I'm confused. I thought that the standard declaration for `operator new(size_t)` was for the exception-throwing version, and you had to do `operator new(size_t, std::nothrow)` to get the null-returning version. – Mike DeSimone Nov 21 '11 at 14:36
  • 2
    Exceptions specifications are not intended to declare the *presence* of exceptions, but the *absence* of any other kind of exceptions. And even then, it's a runtime check... – Matthieu M. Nov 21 '11 at 14:37
  • @MikeDeSimone: you are right. The standard form takes just the size as argument, any other form may exist, and the supplementary arguments have to be explicitly passed at invocation `new(std::nothrow()) T()` for the no throw version. – Matthieu M. Nov 21 '11 at 14:38
  • What is more suprising, is that most compilers (including GCC) don't suppose that `this` is never nil when optimizing & inlining. – Basile Starynkevitch Nov 21 '11 at 17:19
1

Clang does the optimization you expected:

cccc@~/workspace/tmp$ clang++ --version
Apple clang version 13.1.6 (clang-1316.0.21.2.3)
Target: x86_64-apple-darwin21.4.0
Thread model: posix
InstalledDir: /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin
cccc@~/workspace/tmp$ clang++ test.cc -std=c++11
test.cc:10:40: warning: overflow in expression; result is -2147483648 with type 'int' [-Winteger-overflow]
    void *ptr = malloc(2 * 1024 * 1024 * 1024);
                                       ^
test.cc:15:6: warning: function previously declared with an explicit exception specification redeclared with an implicit exception specification [-Wimplicit-exception-spec-mismatch]
void operator delete(void *ptr) { free(ptr); }
     ^
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/c++/v1/new:182:36: note: previous declaration is here
_LIBCPP_OVERRIDABLE_FUNC_VIS void  operator delete(void* __p) _NOEXCEPT;
                                   ^
2 warnings generated.
cccc@~/workspace/tmp$ ./a.out 
0x0
unreachable
cccc@~/workspace/tmp$ clang++ test.cc -std=c++11 -O3
test.cc:10:40: warning: overflow in expression; result is -2147483648 with type 'int' [-Winteger-overflow]
    void *ptr = malloc(2 * 1024 * 1024 * 1024);
                                       ^
test.cc:15:6: warning: function previously declared with an explicit exception specification redeclared with an implicit exception specification [-Wimplicit-exception-spec-mismatch]
void operator delete(void *ptr) { free(ptr); }
     ^
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/c++/v1/new:182:36: note: previous declaration is here
_LIBCPP_OVERRIDABLE_FUNC_VIS void  operator delete(void* __p) _NOEXCEPT;
                                   ^
2 warnings generated.
cccc@~/workspace/tmp$ ./a.out 
cccc@~/workspace/tmp$ 
Abstract
  • 11
  • 1
  • 1
    Clang made a different optimization: it didn't just optimize away the nullptr check, it optimized away the call to `new` entirely, which is why we don't see the `0x0` from printf either. On https://godbolt.org/z/5d4sE1h5K, we can see that `main` compiled down to just a `return 0`, no calls to new (or malloc or free). But still an interesting observation. Interestingly, even choosing to leak memory isn't treated as an observable side-effect: https://godbolt.org/z/sGcrEqr69 commented out the `delete` and put it in a function other than `main`, still doesn't call `new`. – Peter Cordes May 06 '22 at 15:12
  • Ah, but if we `return ptr`, then the custom `new` does inline (with calls to malloc and printf) and the null-pointer check optimizes away. No conditional branches. https://godbolt.org/z/GP1WGh9zM (This is clang 14 targeting GNU/Linux; clang 10 and earlier do check: https://godbolt.org/z/7G31PcdKj) – Peter Cordes May 06 '22 at 15:15
0

although new should never return a null pointer

It shouldn't in normal operation. But how about a crazy situation when someone plugs out the memory, or it simply died, or it just gets full?

Why would the compiler not eliminate that code? Is it just a missed optimization or is there some other reason for that?

Because new can fail. If you use no-throw version, it can return NULL (or nulptr in c++11).

BЈовић
  • 62,405
  • 41
  • 173
  • 273
0

I checked the assembly produced with g++ -O3 -S and the standard new, gcc (4.4.5) does not remove the if(ptr == 0). It seems that gcc does not have compiler flags or function attributes to optimize NULL checks either.

So it appears that gcc does not support this kind of optimization at the current time.

josefx
  • 15,506
  • 6
  • 38
  • 63