More x86
We already saw condition codes and jumps. Here are some examples of compiled C control flow.
int mult(int a, int b) {
do {
a *= a;
} while(a < b);
return a;
}
// compiles to
mult:
movl %edi, %eax
.L2:
imull %eax, %eax
cmpl %esi, %eax
jl .L2
retWe see that loops are implemented as backwards jumps. For a for-loop, we need to manually check the condition once before we start.
int mult(int a, int i) {
for(; i < 10; i++) {
a *= a;
}
return a;
}
// compiles to
mult:
movl %edi, %eax
cmpl $9, %esi
jg .L2
.L3:
imull %eax, %eax
addl $1, %esi
cmpl $10, %esi
jne .L3
.L2:
retNote that all of these examples are compiler-specific and might change if new optimizations or hardware arise. You can experiment yourself at compiler explorer.
Conditional Moves
Conditional move instructions have the form cmovCx, where C is the condition. Let’s say we only want to move %eax to %edi if it is greater than 10.
compl $10, %eax
cmovgl %eax, %ediSwitches
The compiler has multiple tricks to compile switches efficiently (instead of just doing lots of if-statements in a row). If the switch is sparse (the case-values are far apart), then it can create a binary-tree-like structure. (Basically doing binary search on the values.)
Otherwise is can use jump tables. A jump table stores the addresses of labels in a table in memory. The value (we switch on) is then used as an index into that table to pick the right address.
Say we have a switch like this:
switch(val) {
case 0:
// do sth
break;
case 1:
// do sth else
break;
case 2:
// and so on...
}This will compile to a jump table like this:
# jump table
.L4:
.quad .L10
.quad .L9
.quad .L2
.quad .L8
.quad .L7
.quad .L6
.quad .L5
.quad .L2
.quad .L11
# code
jmp *.L4(,%rsi,8)Then the program first checks it the value is greater than the size of the jump table (and jumps to default). Otherwise it does an indirect jump into the table. (Where is calculates the address based on the offset from the lable. We scale %rsi by 8 here, since each jump table is a pointer of 8 bits.)