Compiler Episode 124 Question

Hey guys,

I have a quick question on some stuff I've seen in episode 124: https://www.youtube.com/watch?v=W_szrzjYuvs&t=818s .

Here Casey is talking about the compiler making decisions and possibly excluding things. Under what context does this happen? I am slightly confused on what he is referring to here. I was under the impression that the code gets compiled and then ran separately and thus the compiler had no impact on runtime decisions in the program. Is he speaking about the compiler excluding stuff at compile-time in prediction of how things may play out at runtime? Thanks.
Todd
Hey guys,

I have a quick question on some stuff I've seen in episode 124: https://www.youtube.com/watch?v=W_szrzjYuvs&t=818s .

Here Casey is talking about the compiler making decisions and possibly excluding things. Under what context does this happen? I am slightly confused on what he is referring to here. I was under the impression that the code gets compiled and then ran separately and thus the compiler had no impact on runtime decisions in the program. Is he speaking about the compiler excluding stuff at compile-time in prediction of how things may play out at runtime? Thanks.


When you write code, you tell the computer WHAT to do. The compiler tells the machine HOW to do it.

Modern compilers have tons of optimization technique to make your code run faster. Also, computer has a memory hierarchy. Fastest is the registers, then L1-4 caches, then main memory, and slowest is harddrive. Running programs are stored in the main memory. Thus, when you want to read something, you have to load it from the main memory.

For a variable that rarely gets modified but being read very often, the compiler can optimize reading by generating machine-level instruction to load the variable into a register(or a cache) and read from the register(or cache). In other word, a copy was made for faster access.

This optimization is not OK in a multi-threading environment. In this situation, we're reading from a snapshot; the actual variable may be modified in the main memory by something else. Using volatile prevents this optimization and force the compiler to load the variable from main memory for each read.

You also mention about compiler excluding stuff at compile time. An example of this is the Return value optimization.

1
2
3
4
int foo(){
   int a = 3+3;
   return a;
}


When compiling, the Compiler may optimize away the variable 'a' and generates machine code closer to this

1
2
3
int foo(){
   return 3+3;
}
Wow thanks for that explanation, that was perfect! It all makes sense now!

Definitely would be interesting to look at the docs and see some of the cool optimization stuff the compiler does, although I'm sure they're beastly.

This show has been interesting and helpful. Some of this stuff actually seems to resemble a literal engine such as a car motor with its pistons all cycling and there can be such things as knock or misfires, etc...
Compiler optimization operate under the "as if" rule. It basically says that it can change you program however it wishes as long as the observable behavior remains the same.

The simplest set of optimizations are the peephole optimizations which takes a few instructions and then optimizes just them, for example changing a multiplication by a constant with a combination of binary shifts and adds or folding operations with constants into a single constant.

Besides that you also have the various code-motion optimizations: loop unrolling, reordering of instructions, inlining functions, pulling a branch out of a loop, common subexpression elimination, etc. These will avoid unnecessary jumps and help other optimizations happen.

The last optimization I'll mention is auto-vectorization which basically means that the compiler will attempt to make a simd version out of your code. This works best with loops where each iteration does not depend on a result from the previous iteration.
Undefined behavior is another thing compilers optimize around.
For example, in the following code:
1
2
3
4
5
6
7
8
void foo(int *a) {
    int x = *a;
    if(a == 0) {
        return;
    } else {
        // Do stuff with x
    }
}

the compiler is allowed to transform it into this:
1
2
3
4
void foo(int *a) {
    int x = *a;
    // Do stuff with x
}


Since a was dereferenced before the null check, and dereferencing a null pointer is undefined behaviour, the compiler is allowed to assume that the pointer is NOT null, and thus can eliminate the "redundant" null check.

A list of examples can be found here.