CompilerDev/Implementing Conditional Statements And Loops
General Concepts
For a conventional instruction set architecture such as x86, ARM, 8051, MIPS, or most other CPU types in widespread use at this time (2016), conditional statements such as if/elsif/else, and loop constructs such as for or while, are generally implemented through a combination of tests, conditional jumps/branches (jz, beq, etc.) and unconditional branches (jmp, bra, j, etc.). While some common ISAs have special-purpose instructions for repetition or looping (e.g., the REP and LOOP instructions on the x86), few compilers use them due to the added work of determining where they can be applied.
Examples below are given for Intel x86-64, ARM, and MIPS R2000 assembly language instruction sets. It is assumed that the compiler produces assembly language; for compilers that generate an Executable File directly, the compiler must handle the housekeeping (e.g., tracking branch targets) that would otherwise be done by the assembler.
General Conditional Statements
If
A naive implementation of a basic if statement will generally consist of a conditional branch to the code for the when the condition is true (the consequent), an unconditional branch past the end of the consequent, and the consequent itself:
if (a == b)
{
/* do something */
}
The generated code (assuming that a and b are in the appropriate registers already) might look like:
| x86-64 | ARM | MIPS |
|---|---|---|
cmp rax, rbx
je main.if0.true
jmp main.if0.end
main.if0.true:
;;; consequent
main.if0.end:
|
teq r0, r1
beq main.if0.true
b main.if0.end
main.if0.true:
;;; consequent
main.if0.end:
|
beq $t0, $t1, main.if0.true
j main.if0.end
main.if0.true:
### consequent
main.if0.end:
|
Note that the compiler has to generate a unique label or target table listing for each of the branch targets; the simplest implementation of this is to keep a count of the labels, and assign them a separate label name with the count appended to it. For the sake of readability, the example code uses a local label with the function name (main, in this case), the type of expression, and the count of the expressions of this type.
Thus, for nested ifs, such as
if (a == b)
{
if (b <= c)
{
/* do the inner clause */
}
/* do the rest of the outer clause */
}
it might produce:
| x86-64 | ARM | MIPS |
|---|---|---|
cmp rax, rbx
je main.if1.true
jmp main.if1.end
main.if1.true:
cmp rbx, rcx
jle main.if2.true
jmp main.if2.end
main.if2.true:
;;; do the inner clause
main.if2.end:
;;; do the rest of the outer clause
main.if1.end:
|
bge $t0, $t1, main.if1.true
j main.if1.end
main.if2.true:
blt $t0, $t2, main.if2.true
j main.if2.end
main.if2.true:
;;; consequent for inner conditional
main.if2.end:
;;; remaining code
main.if1.end:
|
Compound Boolean Conditionals
For compound conditionals, such as logical AND or logical OR, there naive implementation would be to first perform the tests, then use the logical ligatures to produce a testable outcome.
if (!(a <= b && b < c))
/* do something */
}
it could naively generate
| x86-64 | ARM | MIPS |
|---|---|---|
ble $t0, $t1, main.if3.and0.left.true
clear $t3
j main.if3.and0.right
main.if3.and0.left.true:
move $t3, 1
j main.if3.and0.right
main.if3.and0.right:
ble $t0, $t1, main.if3.and0.right.true
clear $t3
j main.if3.and0.test
main.if3.and0.right.true:
move $t3, 1
j main.if3.and0.test
main.if3.and0.test:
and $t3, $t3, $t4
not $t5, $t3
bnez $t5, main.if3.true
j main.if3.end
main.if3.true:
;;; consequent
main.if3.end:
|
While this is a literal translation of the condition, it is clearly less than optimal.
Basic Optimizations
An obvious, and reasonably simple, optimization of this is to negate or reverse the condition, thus eliminating the need for the unconditional branch:
| x86-64 | ARM | MIPS |
|---|---|---|
cmp rax, rbx
jne main.if0.end
;;; consequent
main.if0.end:
|
teq r0, r1
bne main.if0.end
;;; consequent
main.if0.end:
|
bne $t0, $t1, main.if4.end ;;; consequent main.if4.end: |
Similarly, for nested ifs where the inner if is the first or last clause of the outer loop, then it can remove an extraneous labels:
| x86-64 | ARM | MIPS |
|---|---|---|
cmp rax, rbx
jne main.if1.end
;;; if(rax == rbx)
cmp rbx, rcx
jg main.if2.end
;;; if(rbx <= rcx)
;;; do the inner clause
main.if2.end:
;;; do the rest of the outer clause
main.if1.end:
|
blt $t0, $t1, main.if5.end
bge $t0, $t2, main.if6.end
;;; inner consequent
main.if6.end:
;;; remaining code of outer consequent
main.if2.end:
|
For these simple cases, since the mapping of a given operator to its inverse is fixed (e.g., to implement 'less than' you would always substitute 'greater than or equal'), the only change needed is that the code for the test be issued fro the inverse.
Optimizing compound conditionals is a bit trickier, as you would need to consider which operations invert which other ones, and how to apply things like de Morgan's Law. However, it is still reasonable for most compilers to perform certain eliminations, such as replacing separate tests with short-circuiting branches. In the case of an AND', one can start by short-circuiting the first test to the end of the and part conditional, so that the second test only gets checked if the first is true. For the NOT, you can get the right result simply by not inverting the final case:
However, with a little more effort, a better solution might be possible, if we keep a table of boolean functions and their inverses, and are willing to perform a simple analysis of the order of operations.
Definite Loops
The naive implementation of a definite loop is a conditional branch at the start of the loop and an unconditional branch back to that conditional branch at the end of the loop, which is also the naive implementation of an explicit for loop. An example in MIPS assembly code (using pseudo-instructions) might be:
| x86-64 | ARM | MIPS |
|---|---|---|
xor rcx, rcx
mov rbx, for_loop_count
for0.start:
; if rcx is greater than or equal
; to rbx, jump past the loop
cmp rcx, rbx
jge for1.end
;; body of the loop here
inc rcx
jmp for0.start
for0.end:
|
mov r0, #0
mov r1, #for_loop_count
for0.start:
; if r1 is greater than or equal
; to r0, jump past the loop
cmp r0, r1
bge for1.end
;; body of the loop here
add r0, r0, #1
b for0.start
for0.end:
|
clear $t0
move $t1, for_loop_count
for0.start:
; if t1 is greater than or equal
; to t0, jump past the loop
bge $t0, $t1, for1.end
;; body of the loop here
addi $t0, $t0, 1
bra for0.start
for0.end
|
However, even a naive compiler will usually do loop inversion by using an unconditional branch, followed by the loop label, followed by the body, and then put the loop condition as the target of the original unconditional branch with the condition inverted:
| x86-64 | ARM | MIPS |
|---|---|---|
clear $t0
move $t1, for_loop_count
bra for1.test
for1.start:
;; body of the loop here
addi $t0, $t0, 1
for1.test:
; if t1 is less than to t0,
; jump past the loop
blt $t0, $t1, for1.start
for1.end
|
This means that after the loop entry, the loop has just the unconditional branch, making it faster. For a hand-coded recursive descent compiler, this becomes simply a matter of changing the order in which the parsing-emitting functions are called.