简体   繁体   中英

Can a C++ compiler optimize away code when dealing with pointers?

With these two questions as background ( first and second ), I got curious about how much optimization a C++ compiler can perform when dealing with pointers? More specifically, I'm interested in how smart a compiler is when it optimizes away code which it may detect will never be run.

(Some may point out that this is a dupe of this question , which it may be, but this particular part of it was not entirely answered. So therefore I decided to start a new question which deals only with this concern.)

(I'm not a C++ expert, so I may be wrong in the following statements, but I'll give it a shot anyway). A C++ compiler may optimize away portions of code which it will recognize never to be executed or never exited (such as loops). Here is an example:


void test() {
    bool escape = false;

    while ( !escape ); // Will never be exited

    // Do something useful after having escaped
}

The compiler would most likely recognize that the loop will never be exited as the code never changes the value of escape so that the loop is exited. This makes the loop useless.

Now, if we were to change the variable to a pointer, would the compiler still optimize away the loop? Say the code looks like so:


void test( bool* escape ) {
    while ( *escape ); // Will this be executed?

    // Do something useful after having escaped
}

My suspicion is that the compiler will do away with the loop, or else the keyword volatile would be redundant, yes?. But what about when working with threads -- where it is in fact modified but outside the function, and maybe even outside that C++ file entirely -- would the compiler still remove the loop? Does it make a difference if the variable pointed to by escape is a global variable, or a local variable inside another function? Can the compiler make this detection? In this question , some say that the compiler will not optimize the loop if library functions are invoked inside the loop. What mechanisms are then in place when using library functions which prevent this optimization?

In the first case ( while ( !escape ); ) the compiler will treat that as label: goto label; and omit everything after it (and probably give you a warning).

In the second case ( while ( *escape ); ), the compiler has no way of knowing if *escape will be true or false when run, so it has to do the comaprision and loop or not. Note however, it only has to read the value from *escape once, ie, it could treat that as:

 bool b = *escape;
 label:   if (b) goto label;

volatile will force it to read the value from '*escape' each time through the loop.

Remember that the C++ compiler doesn't know, or give two shits, about your threads. Volatile is all you've got. It's perfectly legal for any compiler to make optimizations that destroy multi-threaded code but run fine on single-threaded. When discussing compiler optimizations, ditch threading, it's just not in the picture.

Now, library functions. Of course, your function could have *escape changed at any time by any of those functions, as the compiler has no way to know how they work. This is especially true if you pass the function as a callback. If, however, you have a library where you have the source code, the compiler may dig in and discover that *escape is never changed within.

Of course, if the loop is empty, it will almost certainly just let your program hang, unless it can determine that the condition isn't true when it starts. Removing an empty infinite loop is not the job of the compiler, it's the job of the programmer's brain cells.

The generic problem with a question like this is that they often include a very unrealistic code snippet. A "what will the compiler do" question needs real code. Since compilers were designed and optimized to compile real code. Most compilers will completely eliminate the function, and the function call, since the code has no side-effects. Leaving us with a question that doesn't have a useful answer.

But, sure, you are leaning towards finding a use for the volatile keyword. You can find many threads on SO talking about why volatile isn't appropriate in a multi-threaded app.

There's a difference in what the compiler is allowed to do, and what real compilers do.

The Standard covers this in "1.9 Program execution". The standard describes a sort of abstract machine, and the implementation is required to come up with the same "observable behavior". (This is the "as-if" rule, as documented in a footnote.)

From 1.9(6): "The observable behavior of the abstract machine is its sequence of reads and writes to volatile data and calls to library I/O functions." This means that, if you can show that a modification to a function will cause changes to neither, either in that function or after it's called, the modification is legal.

Technically, this means that if you write a function that will run forever testing (say) Goldbach's Conjecture that all even numbers greater than 2 are the sum of two primes, and only stopping if it finds one that isn't, a sufficiently ingenious compiler could substitute either an output statement or an infinite loop, depending on whether the Conjecture is false or true (or unprovable in the Goedel sense). In practice, it will be a while, if ever, before compilers have theorem provers better than the best mathematicians.

Yes, some compilers are smarter than others. Your first example a decent compiler, optimized or not will see that it does nothing, it may or may not generate code for it, and it may or may not warn you that your code does nothing.

I have seen a compiler that optimized what was many lines of code in different functions called in a loop. I was trying to do a compiler comparison actually, with an lfsr randomizer called repeatedly in a loop (the loop was run a hardcoded number of times). One compiler faithfully compiled (and optimized) the code, performing each functional step, the other compiler figured out what I was doing and the assembler produced was the equivalent of ldr r0,#0x12345 where 0x12345 was the answer if you computed all the variables as many times as the loop was run. One instruction.

As seen in one of your other questions, you are struggling with the use of volatile as well as other specifics of the language. In your second example, with only visibility into that function, the compiler does not know what the memory location that is pointed to is, it could very well be a hardware register that is expected to change at some point, or a memory location shared in some other way by an interrupt or other thread that is expected to change at some point. Without a volatile an optimizer is well within its rights to do something like this:

ldr r0,[r1]
compare:
  cmp r0,#0
  bne compare

Exactly the lesson I learned when I learned about volatile. The memory location was read (once), and then the loop waited for that value to change, just as my code had "told" it to do. Not what I "wanted" it to do (exit the loop when the register being pointed to changed).

Now if you were to do something like this:

void test( bool* escape ) {
    while ( *escape );
}

pretest() {
    bool escape = false;
    test(&escape);
}

Some compilers will faithfully compile this code even though it does nothing (other than burn clock cycles which may be exactly what was desired). Some compilers can look outside one function into another and see that the while(*escape); is never true. And some of those will not put any code in pretest() but for some reason will faithfully include the test() function in the binary even though it is never called by any code. Some compilers will completely remove test() and leave pretest as a simple return. All perfectly valid within these non-real-world-educational examples.

Bottom line your two examples are completely different, the first case what the compiler needs to know to determine the while loop is a nop is all there. In the second case the compiler has no way of knowing the state of escape or if it will ever change and has to faithfully compile the while loop into executable instructions. The only optimization opportunity is whether or not it reads from memory on every pass through the loop.

If you want to really understand what is going on, compile these functions with different optimization options and then disassemble the object code.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM