Introduction
Branching constructs like if-else and switch-case are fundamental control flow structures in C programming. While they appear to execute sequentially from a source code perspective, their actual performance depends heavily on how the compiler translates them to machine instructions. This analysis examines the efficiency of these constructs by examining ARM V7 assembly output, focusing on the specialized instructions that support conditional execution.
When dealing with single integer value comparisons, both structures can often be used interchangeably. Consider the following functional equivalence:
int evaluate_if_else(int value)
{
int result = -1;
if (value == 0)
{
result = 1;
}
else if (value == 1)
{
result = 6;
}
else if (value == 2)
{
result = 3;
}
else
{
result = 2;
}
return result;
}
int evaluate_switch(int value)
{
int result = -1;
switch (value)
{
case 0:
result = 1;
break;
case 1:
result = 6;
break;
case 2:
result = 3;
break;
default:
result = 2;
break;
}
return result;
}
Both functions produce identical behavior. The question becomes: how does the underlying instruction set optimize these branching patterns?
ARM V7 Condition Flags and Branch Instructions
Program Status Register Flags
The ARM V7 architecture organizes its program status register into three distinct components: Application PSR (APSR), Interrupt PSR (IPSR), and Execution PSR (EPSR). The flags relevant to conditional branching reside in the APSR:
| Bit | 31 | 30 | 29 | 28 | 27 | 26:25 | 24 | 23:20 | 19:16 | 15:10 | 9 | 8 | 7 | 6 | 5 | 4:0 |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| xPSR | N | Z | C | V | Q | ICI/IT | T | ICI/IT | Exception Number |
The condition flags (bits 31-28) serve as the foundation for conditional execution:
- N (Negative): Set when the most significant bit of the result indicates a negative value. Reflects the sign of arithmetic operations.
- Z (Zero): Set when the result of an operation equals zero, including data operations, comparisons, and test operations.
- C (Carry): Set when an arithmetic operation produces a carry or borrow, primarily used for unsigned arithmetic.
- V (Overflow): Set when signed arithmetic overflows, such as adding two positive numbers that result in a negative value.
These four flags combine to form 15 distinct branch conditions:
| Condition Code | Meaning | Flags Required |
|---|---|---|
| EQ | Equal | Z == 1 |
| NE | Not equal | Z == 0 |
| CS/HS | Carry set / unsigned >= | C == 1 |
| CC/LO | Carry clear / unsigned < | C == 0 |
| MI | Minus (negative) | N == 1 |
| PL | Plus (non-negative) | N == 0 |
| VS | Overflow set | V == 1 |
| VC | Overflow clear | V == 0 |
| HI | Unsigned > | C == 1 && Z == 0 |
| LS | Unsigned <= | C == 0 || Z == 1 |
| GE | Signed >= | N == V |
| LT | Signed < | N != V |
| GT | Signed > | Z == 0 && N == V |
| LE | Signed <= | Z == 1 && N == V |
| AL/NV | Always/Never | — |
These condition codes append to branch instructions for conditional jumps:
BEQ label ; Branch to label when Z == 1 (equal condition met)
They also modify data movement instructions:
MOVGT R2, R0 ; Move R0 to R2 when the preceding result was greater than
The If-Then Instruction
Instruction Syntax
The IT (If-Then) instruction provides conditional execution for small branch structures:
IT <condition>
ITx <condition>
ITxy <condition>
ITxyz <condition>
The suffix characters T and E represent "Then" and "Else" respectively. The <condition> must reference one of the flag-dependent codes from the condition table (AL/NV excluded). The instruction block supports:
- Atleast one T (the "if" branch)
- Up to three additional T or E qualifiers
- Maximum of 4 instructions total per IT block
The T and E qualifiers must correspond to opposite conditions. Within an IT block, nested IT instructions are prohibited. Additionally, the following instructions cannot appear in IT blocks:
- Conditional branches
- CBZ and CBNZ
- TBB and TBH
- CPS, CPSID, and CPSIE
- SETEND
When <condition> equals AL, all instructions in the block execute unconditionaly, and the E suffix cannot be used.
Practical Examples
The IT instruction optimizes ternary operators and simple two-branch if statements:
int result = 0, input = 0;
// Ternary operator
result = (input == 0) ? 1 : 2;
// Equivalent if-else
if (input == 0)
{
result = 1;
}
else
{
result = 2;
}
Assuming R0 contains the input variable, R1 holds the immediate value 0, and R2 holds the result:
CMP R0, R1 ; Compare input with zero
ITE EQ ; IT block: one "then" instruction (T), one "else" instruction (E)
MOVEQ R2, #1 ; If equal, result = 1
MOVNE R2, #2 ; If not equal, result = 2
A more complex example with multiple instructions per branch:
// Pseudocode
if (R0 == R1)
{
R3 = R4 + R5;
R3 = R3 / 2;
}
else
{
R3 = R6 + R7;
R3 = R3 / 2;
}
; Assembly output
CMP R0, R1
ITTEE EQ ; Two "then" instructions (TT), two "else" instructions (EE)
ADDEQ R3, R4, R5 ; Then-branch: addition
ASREQ R3, R3, #1 ; Then-branch: arithmetic shift right (divide by 2)
ADDNE R3, R6, R7 ; Else-branch: addition
ASRNE R3, R3, #1 ; Else-branch: arithmetic shift right
The IT instruction delivers optimization through two mechanisms:
-
Conditional execution enables the CPU to fetch only instructions satisfying the condition, eliminating wasted cycles on the unselected branch.
-
The instruction replaces conditional branch operations, avoiding pipeline flush penalties and instruction prefetch restarts that occur during normal flow changes.
This optimization applies exclusively to two-branch scenarios.
Table Branch Instructions
TBB and TBH Syntax
For multi-way branching, ARM V7 provides table-based branch instructions:
TBB [Rn, Rm] ; Table branch byte
TBH [Rn, Rm, LSL #1] ; Table branch halfword
These instructions perform indexed table lookups:
- Rn: Base address register containing the branch table start
- Rm: Index register providing the lookup key
- Offset calculation: The instruction reads a value from the table and uses it as a branch offset
For TBB: The table functions as a byte array. The retrieved value branchtablevalue equals *(unsigned char*)(Rn + Rm), and the branch offset becomes branchtablevalue * 2 bytes.
For TBH: The table contains 16-bit entries. The retrieved value equals *(unsigned short*)(Rn + Rm * 2), producing a branch offset of branchtablevalue * 2 bytes.
Both instructions modify the program counter directly:
PC = PC + branchtablevalue * 2
When Rn equals PC, the table base address becomes PC + 4 (accounting for pipeline effects), locating the table immediately after the TBB/TBH instruction.
Month Validation Example
The following code validates whether specific day values are valid for given months:
bool check_date_validity(int month, int day)
{
bool valid = true;
switch (month)
{
case 1:
case 3:
case 5:
case 7:
case 8:
case 10:
case 12:
valid = (day >= 1) && (day <= 31);
break;
case 4:
case 6:
case 9:
case 11:
valid = (day >= 1) && (day <= 30);
break;
case 2:
valid = check_leap_day(day);
break;
default:
valid = false;
break;
}
return valid;
}
The corresponding disassembly demonstrates TBB optimization:
34301e0e: 784b ldrb r3, [r1, #1] ; r3 = month
34301e10: 3b01 subs r3, #1 ; r3 = month - 1
34301e12: 2b0b cmp r3, #11 ; check if month > 12
34301e14: d83a bhi.n 34301e8c ; jump to default handler if out of range
34301e16: e8df f003 tbb [pc, r3] ; table lookup with offset r3
34301e1a: 1606 .short 0x1606 ; branch table start
34301e1c: 0e060e06 .word 0x0e060e06
34301e20: 060e0606 .word 0x060e0606
34301e24: 060e .short 0x060e ; branch table end
; case 1 handler at 0x34301e26
34301e26: 7888 ldrb r0, [r1, #2] ; load day value
34301e28: 3801 subs r0, #1
34301e2a: b2c0 uxtb r0, r0
34301e2c: 281e cmp r0, #30
34301e2e: bf8c ite hi
34301e30: 2000 movhi r0, #0
34301e32: 2001 movls r0, #1
34301e34: 4770 bx lr
; case 4 handler at 0x34301e36
34301e36: 7888 ldrb r0, [r1, #2]
34301e38: 3801 subs r0, #1
34301e3a: b2c0 uxtb r0, r0
34301e3c: 281d cmp r0, #29
34301e3e: bf8c ite hi
34301e40: 2000 movhi r0, #0
34301e42: 2001 movls r0, #1
34301e44: 4770 bx lr
; case 2 handler at 0x34301e46
34301e46: 780b ldrb r3, [r1, #0] ; load year for leap year calculation
34301e48: f013 0f03 tst.w r3, #3
34301e4c: d109 bne.n 34301e62
...
The branch table values map month indices to their corresponding handler offsets:
| Month | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Index (month-1) | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 |
| Branch Offset | 0x06 | 0x16 | 0x06 | 0x0e | 0x06 | 0x0e | 0x06 | 0x06 | 0x0e | 0x06 | 0x0e | 0x06 |
Months with 31 days share identical table entries, as do months with 30 days. The February case requires special handling due to leap year complexity.
The TBB instruction transforms the switch structure into a hash-like lookup mechanism. Instead of sequentially comparing the input value against each case label and jumping to matching code, the processor directly indexes into the table and branches to the appropriate handler.
This optimization carries limitations. The approach works optimally when case values form a compact, contiguous range. When values are sparse or span an enormous range—such as values like 1, 15, 5689, and 14567—compilers typically avoid TBB/TBH optimization.
Performance Characteristics
From a C language perspective, both if-else and switch constructs appear to execute sequentially. Actual performance depends on how the compiler optimizes these structures at the assembly level.
For two-branch scenarios, the IT instruction ensures efficient if-else execution through conditional code execution.
For multi-branch cases with compact, consecutive integer inputs, switch statements leverage TBB/TBH instructions for direct table-based branching, typically outperforming sequential if-else chains.
When inputs are sparse or non-integer based, if-else remains the appropriate choice, as compilers cannot apply table branch optimization in these cases.