as-if rule and removal of allocation
Asked Answered
W

1

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.

Wardieu answered 20/1, 2016 at 18:40 Comment(11)
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.Kobylak
@Kobylak 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.Oeillade
[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.Kobylak
@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.Wardieu
@Oeillade " 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.Wardieu
@Oeillade 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: goo.gl/lCCyBx . As you can see compiler optimised away memory allocation.Kobylak
@Kobylak Thanks for the note. In that case, volatile would help with allocation preservation but not with reordering.Oeillade
@JAB: Why do you need the otherwise useless allocation to occur?Benniebenning
@Benniebenning 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.Oeillade
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.Mobility
@Kobylak 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.Mobility
H
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.

Henton answered 20/1, 2016 at 19:3 Comment(9)
Which of the several questions does your "No." answer?Langtry
@BenVoigt I see two ?. I have now answered them in order.Henton
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.Wardieu
@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.Henton
Yakk, does it actually allow to throw std::bad_allloc by itself? I thought the std::bad_alloc is thrown by the allocation function.Mobility
@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?Henton
@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?Wardieu
@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?Mobility
@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.Henton

© 2022 - 2024 — McMap. All rights reserved.