c++c++14as-if

as-if rule and removal of allocation


The "as-if rule" gives the compiler the right to optimize out or reorder expressions that would not make a difference to the output and correctness of a program under certain rules, such as;

§1.9.5

A conforming implementation executing a well-formed program shall produce the same observable behavior as one of the possible executions of the corresponding instance of the abstract machine with the same program and the same input.

The cppreference url I linked above specifically mentions special rules for the values of volatile objects, as well as for "new expressions", under C++14:

New-expression has another exception from the as-if rule: the compiler may remove calls to the replaceable allocation functions even if a user-defined replacement is provided and has observable side-effects.

I assume "replaceable" here is what is talked about for example in

§18.6.1.1.2

Replaceable: a C++ program may define a function with this function signature that displaces the default version defined by the C++ standard library.

Is it correct that mem below can be removed or reordered under the as-if rule?

  {
  ... some conformant code // upper block of code

  auto mem = std::make_unique<std::array<double, 5000000>>();

  ... more conformant code, not using mem // lower block of code
  }

Is there a way to ensure it's not removed, and stays between the upper and lower blocks of code? A well placed volatile (either/or volatile std::array or left of auto) comes to mind, but as there is no reading of mem, I think even that would not help under the as-if rule.

Side note; I've not been able to get visual studio 2015 to optimize out mem and the allocation at all.

Clarification: The way to observe this would be that the allocation call to the OS comes between any i/o from the two blocks. The point of this is for test cases and/or trying to get objects to be allocated at new locations.


Solution

  • Yes; No. Not within C++.

    The abstract machine of C++ does not talk about system allocation calls at all. Only the side effects of such a call that impact the behavior of the abstract machine are fixed by C++, and even then the compiler is free to do something else, so long as-if it results in the same observable behavior on the part of the program in the abstract machine.

    In the abstract machine, auto mem = std::make_unique<std::array<double, 5000000>>(); creates a variable mem. It, if used, gives you access to a large amount of doubles packed into an array. The abstract machine is free to throw an exception, or provide you with that large amount of doubles; either is fine.

    Note that it is a legal C++ compiler to replace all allocations through new with an unconditional throw of an allocation failure (or returning nullptr for the no throw versions), but that would be a poor quality of implementation.

    In the case where it is allocated, the C++ standard doesn't really say where it comes from. The compiler is free to use a static array, for example, and make the delete call a no-op (note it may have to prove it catches all ways to call delete on the buffer).

    Next, if you have a static array, if nobody reads or writes to it (and the construction cannot be observed), the compiler is free to eliminate it.


    That being said, much of the above relies on the compiler knowing what is going on.

    So an approach is to make it impossible for the compiler to know. Have your code load a DLL, then pass a pointer to the unique_ptr to that DLL at the points where you want its state to be known.

    Because the compiler cannot optimize over run-time DLL calls, the state of the variable has to basically be what you'd expect it to be.

    Sadly, there is no standard way to dynamically load code like that in C++, so you'll have to rely upon your current system.

    Said DLL can be separately written to be a noop; or, even, you can examine some external state, and conditionally load and pass the data to the DLL based on the external state. So long as the compiler cannot prove said external state will occur, it cannot optimize around the calls not being made. Then, never set that external state.

    Declare the variable at the top of the block. Pass a pointer to it to the fake-external-DLL while uninitialized. Repeat just before initializing it, then after. Then finally, do it at the end of the block before destroying it, .reset() it, then do it again.