65

As far as compiler optimizations go, is it legal and/or possible to change a heap allocation to a stack allocation? Or would that break the as-if rule?

For example, say this is the original version of the code

{
    Foo* f = new Foo();
    f->do_something();
    delete f;
}

Would a compiler be able to change this to the following

{
    Foo f{};
    f.do_something();
}

I wouldn't think so, because that would have implications if the original version was relying on things like custom allocators. Does the standard say anything specifically about this?

Cory Kramer
  • 114,268
  • 16
  • 167
  • 218
  • 70
    No, that goes too far. Growing stack usage is a big deal, they did name a popular programming web site after it. – Hans Passant Nov 02 '17 at 12:58
  • 4
    [Related](https://stackoverflow.com/q/31873616/3484570). – nwp Nov 02 '17 at 12:59
  • Sometimes there are very good reasons to use the heap / stack; in particular for embedded systems. – UKMonkey Nov 02 '17 at 13:00
  • 2
    related: https://stackoverflow.com/questions/47072261/why-isnt-this-unused-variable-optimised-away/47072619#47072619 – 463035818_is_not_an_ai Nov 02 '17 at 13:03
  • 9
    Clang does optimize this iff it can inline the function that's called (+ some conditions on the function body probably). https://godbolt.org/g/hnAMTZ – Baum mit Augen Nov 02 '17 at 13:18
  • 5
    from the link mentioned by tobi303, things have changed since c++14, see [[expr.new](http://eel.is/c++draft/expr.new#10)]; from c++14 on, the compiler can store Foo in the stack as long as it can prove the same behaviour (eg. nothing is thrown in do_something) – Massimiliano Janes Nov 02 '17 at 13:21
  • @UKMonkey Good point, but one could further imagine a situation where `f` would not even be "allocated" *at all*, because its data members would end up being in registers or completely optimized away. In that case there is no advantage whatsoever to dynamic allocation, except for the obvious fact that the requested heap allocation was never performed. One could reasonably asked whether the compiler would *then* be allowed to eliminate it. – user4815162342 Nov 02 '17 at 13:38
  • @MassimilianoJanes: I've just skimmed through `expr.new`, and I didn't see limitations about whether `do_something` throws. Do you see something which prohibits replacing new when `do_something` throws? If yes, maybe it's worth including in my answer. – geza Nov 02 '17 at 13:59
  • 1
    @geza as I read it, the implementation is allowed to omit the allocation, but it's not allowed to arbitrarily add new sideffects as a consequence of such omission; that is, it can store Foo on the stack but it cannot call the destructor if do_something() throws; hence the two code snippets are not equivalent if the compiler cannot prove that do_something doesn't throw – Massimiliano Janes Nov 02 '17 at 14:09
  • @MassimilianoJanes; ah, I see. I think I'll include this information in my answer, thanks! – geza Nov 02 '17 at 14:16

3 Answers3

54

Yes, it's legal. expr.new/10 of C++14:

An implementation is allowed to omit a call to a replaceable global allocation function (18.6.1.1, 18.6.1.2). When it does so, the storage is instead provided by the implementation or provided by extending the allocation of another new-expression.

expr.delete/7:

If the value of the operand of the delete-expression is not a null pointer value, then:

— If the allocation call for the new-expression for the object to be deleted was not omitted and the allocation was not extended (5.3.4), the delete-expression shall call a deallocation function (3.7.4.2). The value returned from the allocation call of the new-expression shall be passed as the first argument to the deallocation function.

— Otherwise, if the allocation was extended or was provided by extending the allocation of another new- expression, and the delete-expression for every other pointer value produced by a new-expression that had storage provided by the extended new-expression has been evaluated, the delete-expression shall call a deallocation function. The value returned from the allocation call of the extended new-expression shall be passed as the first argument to the deallocation function.

— Otherwise, the delete-expression will not call a deallocation function (3.7.4.2).

So, in summary, it's legal to replace new and delete with something implementation defined, like using the stack instead of heap.

Note: As Massimiliano Janes comments, the compiler could not stick exactly to this transformation for your sample, if do_something throws: the compiler should omit destructor call of f in this case (while your transformed sample does call the destructor in this case). But other than that, it is free to put f into the stack.

geza
  • 28,403
  • 6
  • 61
  • 135
  • 2
    The question is if the allocation can be on stack even if `new` used. If I understand correctly it will always be dynamic memory and never on the stack. the given paragraph say that the size of allocation can be extended to a larger block of dynamic memory, or use previous extention of dynamic memory. – SHR Nov 02 '17 at 13:55
  • 2
    @SHR: I've put emphasis on "storage is instead provided by the implementation". It can be anything, even the stack. – geza Nov 02 '17 at 14:00
  • 1
    The background to these changes is discussed in my answer to [Is the compiler allowed to optimize out heap memory allocations?](https://stackoverflow.com/a/31877074/1708801) – Shafik Yaghmour Nov 03 '17 at 16:42
6

These are not equivalent. f.do_something() might throw, in which case the first object remains in memory, the second gets destructed.

lorro
  • 10,687
  • 23
  • 36
  • 8
    Worth noting that declaring the function `noexcept` does not help the optimizers of gcc and clang, but showing clang the function body does. There is probably more to this. – Baum mit Augen Nov 02 '17 at 13:33
  • @BaummitAugen If you're asking "why do compilers not perform this optimization" I think there is indeed more to it: when someone writes a new-expression they want dynamic allocation. If they wanted stack allocation they would have written `Foo f{}`. There are valid reasons for this and the compiler cannot know, e.g. perhaps they are running under valgrind and want to track all heap usages, or perhaps they are debugging a heap fragmentation problem. The compiler has to strike a balance between permitted optimizations, and what coders really want. The compiler should be a friend, not a foe – M.M Nov 03 '17 at 00:32
3

I'd like to point out something IMO not stressed enough in the other answers:

struct Foo {
    static void * operator new(std::size_t count) {
        std::cout << "Hey ho!" << std::endl;
        return ::operator new(count);
    }
};

An allocation new Foo() cannot generally be replaced, because:

An implementation is allowed to omit a call to a replaceable global allocation function (18.6.1.1, 18.6.1.2). When it does so, the storage is instead provided by the implementation or provided by extending the allocation of another new-expression.

Thus, like in the Foo example above, the Foo::operator new needs to be called. Omitting this call would change the observable behavior of the program.

Real world example: Foos might need to reside in some special memory region (like memory mapped IO) to function properly.

Daniel Jour
  • 15,896
  • 2
  • 36
  • 63