-->

The as-if rule

By: Priya Philip 2 months, 2 weeks ago

Allows any and all code transformations that do not change the observable behavior of the program.

Explanation

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

1) At every sequence point, the values of all volatile objects are stable (previous evaluations are complete, new evaluations not started). (until C++11)

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. (since C++11)​

2) At program termination, data written to files is exactly as if 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).

3) Prompting text which is sent to an interactive devices will be shown before the program waits for input.
4) If #pragma STDC FENV_ACCESS is set to ON, the changes to thefloating-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 intermediate results of any expression may be calculated as if to infinite range and precision (unless #pragma STDC FP_CONTRACT is OFF)

Notes

Because the compiler is (usually) unable to analyze the code of an external library to determine whether it does or does not perform I/O or volatile access, third-party library calls also aren't affected by optimization. However, standard library calls may be replaced by other calls, eliminated, or added to the program during optimization. Statically-linked third-party library code may be subject to link-time optimization.

Programs with undefined behavior, e.g. due to access to an array out of bounds, modification of a const object, evaluation order violations, etc, are free from the as-if rule: they often change observable behavior when recompiled with different optimization settings. For example, if a test for signed integer overflow relies on the result of that overflow, e.g. if(n+1 < n) abort();,it is removed entirely by some compilers becausesigned overflow is undefined behavior and the optimizer is free to assume it never happens and the test is redundant.

Copy elision is the only well-defined exception from the as-if rule.

Copy elision optimizes out copy- and move-constructors, resulting in zero-copy pass-by-value semantics. It is the only allowed form of optimization that can change the observable side-effects. Because some compilers do not perform copy elision in every situation where it is allowed (e.g., in debug mode), programs that rely on the side-effects of copy/move constructors and destructors are not portable.

Even when copy elision takes place and the copy-/move-constructor is not called, it must be present and accessible (as if no optimization happened at all), otherwise the program is ill-formed.

The count and order of floating-point exceptions can be changed by optimization as long as the state as observed by the next floating-point operation is as if no optimization took place:

#pragma STDC FENV_ACCESS ON
for (i = 0; i < n; i++) x + 1; // x+1 is dead code, but may raise FP exceptions
// (unless the optimizer can prove otherwise). However, executing it n times will
// raise the same exception over and over. So this can be optimized to:
if (0 < n) x + 1;

Example

int& preinc(int& n) { return ++n; }
int add(int n, int m) { return n+m; }
 
// volatile input to prevent constant folding
volatile int input = 7;
 
// volatile output to make the result a visible side-effect
volatile int result;
 
int main()
{
    int n = input;
// using built-in operators would invoke undefined behavior
//    int m = ++n + ++n;
// but using functions makes sure the code executes as-if 
// the functions were not overlapped
    int m = add(preinc(n), preinc(n));
    result = m;
}

Output:

# full code of the main() function as produced by the GCC compiler
# x86 (Intel) platform:
        movl    input(%rip), %eax   # eax = input
        leal    3(%rax,%rax), %eax  # eax = 3 + eax + eax
        movl    %eax, result(%rip)  # result = eax
        xorl    %eax, %eax          # eax = 0 (the return value of main())
        ret
 
# PowerPC (IBM) platform:
        lwz 9,LC..1(2)
        li 3,0          # r3 = 0 (the return value of main())
        lwz 11,0(9)     # r11 = input;
        slwi 11,11,1    # r11 = r11 << 1;
        addi 0,11,3     # r0 = r11 + 3;
        stw 0,4(9)      # result = r0;
        blr
 
# Sparc (Sun) platform:
        sethi   %hi(result), %g2
        sethi   %hi(input), %g1
        mov     0, %o0                 # o0 = 0 (the return value of main)
        ld      [%g1+%lo(input)], %g1  # g1 = input
        add     %g1, %g1, %g1          # g1 = g1 + g1
        add     %g1, 3, %g1            # g1 = 3 + g1
        st      %g1, [%g2+%lo(result)] # result = g1
        jmp     %o7+8
        nop
 
# in all cases, the side effects of preinc() were eliminated, and the
# entire main() function was reduced to the equivalent of result = 2*input + 3;

Comments

Wish to hire us?

We take the vision which comes from dreams and apply the magic of science and mathematics, adding the heritage of our profession and our knowledge to create a design.