9

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.

Johan Lundberg
  • 26,184
  • 12
  • 71
  • 97
  • 1
    I believe volatile would help there, but there is another problem: nothing depends on mem value, so compiler can move allocation anywhere in that block of code. It might allocate at the very beginning, at the end and in any other place. – Revolver_Ocelot Jan 20 '16 at 18:43
  • @Revolver_Ocelot I don't think the volatile will even make a difference. `make_unique` creates a new object, and as far as I know C++ compilers do _not_ optimize out object creation due to the potential for side effects. They may elide unnecessary constructor calls, but they always ensure at least one is called. Otherwise, code that uses objects for RAII having constructors with side effects could not safely rely on that pattern without the inclusion of memory barriers. – JAB Jan 20 '16 at 18:53
  • **[intro.execution]/8** says "_Access to volatile objects are evaluated strictly according to the rules of the abstract machine_". I read it as "as-if rule cannot be applied to volatile objects", so it should help. – Revolver_Ocelot Jan 20 '16 at 18:54
  • @Revolver_Ocelot, I'm not sure there is any access to a volatile object. And even if there is, non-volatile access from the two blocks may (as far as I understand) still move past the volatile access. – Johan Lundberg Jan 20 '16 at 18:58
  • @JAB " the potential for side effects." I think that is changed in C++14, and I think that's what the quote I make regarding that is about. – Johan Lundberg Jan 20 '16 at 18:58
  • 1
    @JAB nor std::array, nor double construction has any side effects, and memory allocation itself is allowed to be optimised away even in presence of side effects. RAII works exactly because there are side effects (and if there are none, you would not be able to check if it is there or was). Proof: http://goo.gl/lCCyBx . As you can see compiler optimised away memory allocation. – Revolver_Ocelot Jan 20 '16 at 18:58
  • @Revolver_Ocelot Thanks for the note. In that case, volatile would help with allocation preservation but not with reordering. – JAB Jan 20 '16 at 19:01
  • @JAB: Why do you need the otherwise useless allocation to occur? – GManNickG Jan 20 '16 at 19:03
  • @GManNickG You mean for the example provided in the question, which does not involve obvious side effects other than heap allocation? I have no idea, unless the intent is to provide a region of memory for another process to use (which would be pointless if the process allocating it doesn't do anything with it and the allocation goes away at the end of scope). There might be some architecture where such an allocation may avoid an esoteric bug, but in the general case it is indeed not necessary. If the compiler can determine ctors/dtors have no side effects then I guess it can just remove them. – JAB Jan 20 '16 at 22:10
  • Why do you use `std::array` as the example? I thought that it doesn't use `new`. Or is it allowed to use `new` internally? Then that class would be pretty useless IMO. – Johannes Schaub - litb Jan 23 '16 at 12:11
  • @Revolver_Ocelot I am unsure as to how far the `volatile` in here goes: `new volatile int(10)`. The spec says that access to volatiles is the observable side effect. Not the call to the allocation function. So the compiler is still allowed to omit the call to the allocation function if it arranges to allocate the memory elsewhere!? Only judging by that cppreference quote.. I have not looked up the spec about it. – Johannes Schaub - litb Jan 23 '16 at 12:14

1 Answers1

4

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.

Yakk - Adam Nevraumont
  • 262,606
  • 27
  • 330
  • 524
  • Which of the several questions does your "No." answer? – Ben Voigt Jan 20 '16 at 19:05
  • 2
    @BenVoigt I see two `?`. I have now answered them in order. – Yakk - Adam Nevraumont Jan 20 '16 at 19:06
  • 1
    What you mention about `throw` is interesting, because the exception would be observable outside of that block of code, and can not come before side effects of the the upper code, or after side effects of the lower. – Johan Lundberg Jan 20 '16 at 19:20
  • 1
    @JohanLundberg yes, except the compiler is *not mandated to throw* in that case. It is *allowed* to throw, either sometimes, based on resources, the phase of the moon, always, or even never. All are valid ways to respond to a call to `new` under the C++ standard. As an optimization, removing the possibility to throw is legal (by not allocating). The "always throw" bit was simply an illustration of the extreme latitude the C++ standard gives the implementation. – Yakk - Adam Nevraumont Jan 20 '16 at 19:22
  • Yakk, does it actually allow to throw std::bad_allloc by itself? I thought the std::bad_alloc is thrown by the allocation function. – Johannes Schaub - litb Jan 23 '16 at 12:15
  • @joh I do not understand. Can you clarify that question? What is "it"? "`new`"? And are you saying the C++ standard talks about a distinct "allocation function" step which throws? Or are you talking about allocators? – Yakk - Adam Nevraumont Jan 23 '16 at 13:18
  • @JohannesSchaub-litb. I picked array for that reason, that it does not do any of it's own allocation. You can replace it with int for the sake of argument. On the other question: make_unique does call `new` and it may throw `std::bad_alloc`. I also do agree that `auto x = new volatile int(10)` may be removed if x is never used. I think the same applies to any `new (possibly volatile) T(args)` for T with constructor and destructor without visible side effects. For allocators, side effects does not prevent removal under the `as-if` according to my cppref quote. Agree? – Johan Lundberg Jan 23 '16 at 18:03
  • @Yakk you have said "Note that it is a legal C++ compiler to replace all allocations through new with an unconditional throw of an allocation failure". I.e that the new operator by itself can throw, independent of the allocation function. But if the corresponding allocation function wouldn't throw, wouldn't that be a non-conforming implementation? – Johannes Schaub - litb Jan 23 '16 at 18:06
  • @joh I was talking about the global default operator new, which is an allocation function `[basic.stc.dynamic]3.7.4/1`. I was stating, with less precision, that it could always throw an allocation failure on every request. – Yakk - Adam Nevraumont Jan 23 '16 at 19:25