What Exactly Is the "As-If" Rule

What exactly is the as-if rule?

What is the "as-if" rule?

The "as-if" rule basically defines what transformations an implementation is allowed to perform on a legal C++ program. In short, all transformations that do not affect a program's "observable behavior" (see below for a precise definition) are allowed.

The goal is to give implementations freedom to perform optimizations as long as the behavior of the program remains compliant with the semantics specified by the C++ Standard in terms of an abstract machine.


Where does the Standard introduce this rule?

The C++11 Standard introduces the "as-if" rule in Paragraph 1.9/1:

The semantic descriptions in this International Standard define a parameterized nondeterministic abstract
machine. This International Standard places no requirement on the structure of conforming implementations.
In particular, they need not copy or emulate the structure of the abstract machine. Rather, conforming
implementations are required to emulate (only) the observable behavior of the abstract machine as explained
below.

Also, an explanatory footnote adds:

This provision is sometimes called the “as-if” rule, because an implementation is free to disregard any requirement of this
International Standard as long as the result is as if the requirement had been obeyed, as far as can be determined from the
observable behavior of the program. For instance, an actual implementation need not evaluate part of an expression if it can
deduce that its value is not used and that no side effects affecting the observable behavior of the program are produced.


What does the rule mandate exactly?

Paragraph 1.9/5 further specifies:

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
. However, if any such execution contains an undefined operation, this International
Standard places no requirement on the implementation executing that program with that input (not even
with regard to operations preceding the first undefined operation).

It is worth stressing that this constraint applies when "executing a well-formed program" only, and that the possible outcomes of executing a program which contains undefined behavior are unconstrained. This is made explicit in Paragraph 1.9/4 as well:

Certain other operations are described in this International Standard as undefined (for example, the effect
of attempting to modify a const object). [ Note: This International Standard imposes no requirements on
the behavior of programs that contain undefined behavior
. —end note ]

Finally, concerning the definition of "observable behavior", Paragraph 1.9/8 goes as follows:

The least requirements on a conforming implementation are:

— Access to volatile objects are evaluated strictly according to the rules of the abstract machine.

— At program termination, all data written into files shall be identical to one of the possible results that
execution of the program according to the abstract semantics would have produced.

— The input and output dynamics of interactive devices shall take place in such a fashion that prompting
output is actually delivered before a program waits for input. What constitutes an interactive device
is implementation-defined.

These collectively are referred to as the observable behavior of the program. [ Note: More stringent
correspondences between abstract and actual semantics may be defined by each implementation. —end
note
]


Are there situations where this rule does not apply?

To the best of my knowledge, the only exception to the "as-if" rule is copy/move elision, which is allowed even though the copy constructor, move constructor, or destructor of a class have side effects. The exact conditions for this are specified in Paragraph 12.8/31:

When certain criteria are met, an implementation is allowed to omit the copy/move construction of a class
object, even if the constructor selected for the copy/move operation and/or the destructor for the object
have side effects
. [...]

Understanding the as-if rule, the program was executed as written

On Monday your boss comes into your office and says "I need file A on my desk by Thursday and file B on my desk on Friday". He first describes the things that he wants in file A and how he thinks you should do those and then describes the things he wants in file B.

In the mind of your boss, you will first do the things for file A, place that file on his desk on Thursday, then get to work on file B and finish that on Friday. But you realize that it would make more sense to start work on file B earlier - before file A even. There's no reason your boss has to know - all he cares about is receiving A on Thursday and B on Friday. You also realize that the way he suggested can be improved, so you take a slightly different approach to producing the required information.

In this analogy, the boss is some C++ code and you are the compiler. It is legal for the compiler to rearrange operations (work on the files in another order) as long as the observable behavior (putting files on the desk of the boss) is the same. Similarly, the compiler is free to do any transformations (using a different approach than the one described by the boss) on the code that preserve the observable behavior.

In particular, "as if the program was executed as written" means "as if you did the work exactly as your boss instructed you to" (even if you did something different).

as-if rule and removal of allocation

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.

Where are the statement of or the foundations for the as if rule in the C++ Standard?

The definition for "as if" rule is given in the footnote to 4.1.1/1. See http://eel.is/c++draft/intro.abstract#footnote-4:

This provision is sometimes called the “as-if” rule, because an
implementation is free to disregard any requirement of this document
as long as the result is as if the requirement had been obeyed, as far
as can be determined from the observable behavior of the program. For
instance, an actual implementation need not evaluate part of an
expression if it can deduce that its value is not used and that no
side effects affecting the observable behavior of the program are
produced.

Timing vs the as-if rule

For the As-If rule to apply, the compiler must prove that the proposed change has no impact on the observable behavior. You are correct that the passage of time is not an observable behavior. However, in the case of reordering functions, it must prove that the order the functions are called in doesn't impact the observable behavior.

Using timing features will invariably involve some mechanism for measuring time which the compiler will not be able to prove is safe to reorder. For example, it might involve a call to an opaque system API function or driver function that it can't inspect. If we take the most transparent example, a monotonic software clock that simply advances by 1 unit of time every time it's state is taken, there's no way to prove that the call order doesn't matter because it does matter.

Is store reordering allowed by C++ as-if rule?

If cppreference.com disagrees with the actual text of the C++ standard, cppreference.com is wrong. The only things that can supersede the text of the standard are a newer version of the standard, and official resolutions of defect reports (which sometimes get rolled up into documents called "technical corrigienda", which is a fancy name for a minor release of the standard).

However, in this case, you have misunderstood what cppreference.com means by "input and output operations". (If memory serves, that text is taken verbatim from an older version of the standard.) Stores to memory are NOT output operations. Only writing to a file (that is, any stdio.h or iostream output stream, or other implementation-defined mechanism, e.g. a Unix file descriptor) counts as output for purposes of this rule.

The C and C++ standards, prior to their 2011 revisions, assumed a single-threaded abstract machine, and therefore did not bother specifying anything about store ordering, because there was no way to observe stores out of program order. C(++)11 added a whole bunch of rules for store ordering as part of the new multithreading specification.

Does as-if rule prevent compiler reordering of accesses to global/member variables?

Absolutely they may. The compiler has no obligation whatsoever to consider side effects to other threads or hardware.

The compiler is only forced to consider this if you use volatile or synchronization (and those two are not interchangable).

The Standard memory model is known as SC-DRF, or Sequentially Consistent Data Race Free. A data race would be exactly the scenario you've just described- where one thread is observing non-synchronized variables whilst another is mutating them. This is undefined behaviour. Effectively, the Standard explicitly gives the compiler the freedom to assume that there are no other threads or hardware which is reading non-volatile non-synchronized variables. It is absolutely legitimate for a compiler to optimize on that basis.

By the way, that link is kinda crap. His "fix" does not fix anything at all. The only correct fix for multi-threaded code is to use synchronization.

Does as-if rule allow this kind of execution reordering

Reordering and multithreading are 2 different things:

Via multi threading, possible outputs are:

  • "a - 1", "a - 2", "b - 1", "b - 2"
  • "a - 1", "b - 1", "a - 2", "b - 2"
  • "a - 1", "b - 1", "b - 2", "a - 2"
  • "b - 1", "b - 2", "a - 1", "a - 2"
  • "b - 1", "a - 1", "b - 2", "a - 2"
  • "b - 1", "a - 1", "a - 2", "b - 2"

You just have the guaranty that "a - 1" comes before "a - 2", and "b - 1" comes before "b - 2". (unrelated to reordering)

Reordering with the as-if rule is just an optimization.

as_if states:

The C++ compiler is permitted to perform any changes to the program as long as the following remains true:

  1. Accesses (reads and writes) to volatile objects occur strictly according to the semantics of the expressions in which they occur. In particular, they are not reordered with respect to other volatile accesses on the same thread.
    (since C++11)
  2. At program termination, data written to files is exactly as if the program was executed as written.
  3. Prompting text which is sent to interactive devices will be shown before the program waits for input.
  4. If the ISO C pragma #pragma STDC FENV_ACCESS is supported and is set to ON, the changes to the floating-point environment (floating-point exceptions and rounding modes) are guaranteed to be observed by the floating-point arithmetic operators and function calls as if executed as written, except that
    the result of any floating-point expression other than cast and assignment may have range and precision of a floating-point type different from the type of the expression (see FLT_EVAL_METHOD)
    notwithstanding the above, intermediate results of any floating-point expression may be calculated as if to infinite range and precision (unless #pragma STDC FP_CONTRACT is OFF)

Your code (except potentially the // do something) does nothing of that, so it can even drop the 2 code completely, push "c - 1".

But if after you print queue content, then the content should be one of the 6 shown above.



Related Topics



Leave a reply



Submit