Conditionals: the art of making decisions in the machine
Conditionals: the art of making decisions in the machine
In the world of programming, there are concepts that, because of their apparent simplicity, go unnoticed. Conditionals are one of them. An if is the first thing we learn, we use it daily, and we rarely stop to think about what really happens when the processor faces a fork in the road. However, behind that instruction lies one of the most fascinating and complex pieces of machinery in computer architecture.
This article is the second in a series where we break down the basic concepts of programming until they are reduced to their purest form: what actually happens in the hardware when we write a line of code. If in the first article we explored the variable as the fundamental unit of memory, today we will explore the conditional as the fundamental unit of decision-making.
1. What is a conditional?
A conditional is a control structure that allows a program to execute different blocks of code depending on whether a condition is met or not. In essence, it is how we tell the machine: "If this is true, do this; otherwise, do that." It is the mechanism that turns a program into something more than a linear sequence of instructions.
In most programming languages, the most basic conditional is if. Its simplest form is:
if (condition) {
// block of code that runs if the condition is true
}
But conditionals take many forms: if-else, else if, switch, match, and ternary expressions. All of them share the same principle: evaluate a boolean condition (true or false) and divert the flow of execution based on the result.
2. The conditional at a low level: the machine that decides
To truly understand what a conditional is, we must descend to the assembly level and, beyond that, to the hardware level. At bottom, a conditional reduces to a comparison operation followed by a conditional jump.
2.1. Condition flags: the processor's traffic lights
The processor has no abstract concept of "true" or "false." What it has is a status register, called the PSR (Program Status Register) or FLAGS, which contains a series of bits indicating the result of the last arithmetic or logical operation. The most important are:
- ZF (Zero Flag): set when the result of an operation is zero.
- SF (Sign Flag): set when the result is negative.
- CF (Carry Flag): set when there is a carry in an arithmetic operation.
- OF (Overflow Flag): set when there is an overflow.
When the processor executes an instruction like CMP (compare), which internally performs a subtraction without saving the result, it updates these flags. For example, if we compare two equal values, the result of the subtraction is zero, and the ZF flag is set. If the first value is less than the second, the result is negative, and the SF flag is set.
flowchart TD
A["CMP instruction(compares two values)"] --> B["ALU performs subtractionwithout saving the result"]
B --> C{"Result = 0?"}
C -->|Yes| D["ZF = 1"]
C -->|No| E["ZF = 0"]
B --> F{"Result negative?"}
F -->|Yes| G["SF = 1"]
F -->|No| H["SF = 0"]
2.2. The conditional jump instruction
Once the flags are set, the processor executes a conditional jump instruction (such as JE, JNE, JG, JL, etc.) that checks the flags and decides whether to jump to a different memory address or continue with the next instruction.
The following flowchart shows how a simple if translates into assembly instructions.
flowchart TD
A["C code:if (x > 5) { y = 1;}"] --> B["Assembly:CMP x, 5"]
B --> C["JG .L1"]
C --> D["(if x <= 5, no jump)y = 0"]
D --> E["JMP .L2"]
C --> F[".L1:(if x > 5, jump)y = 1"]
F --> E[".L2:program continues"]
In this example, CMP x, 5 compares the value of x with 5 and updates the flags. Then, JG .L1 (Jump if Greater) jumps to label .L1 only if x is greater than 5. If not, execution continues with the next instruction (which in this case assigns y = 0). At the end, JMP .L2 unconditionally jumps to the end of the block.
2.3. The cost of the jump: the pipeline and branch prediction
Here is where things get interesting. Modern processors do not execute one instruction at a time. They use a pipeline that divides execution into stages (fetch, decode, execute, memory, write-back) and processes several instructions simultaneously at different stages.
The problem is that a conditional jump breaks this pipeline. The processor does not know whether the jump will be taken until the comparison instruction has passed through the execute stage. Meanwhile, it has been fetching subsequent instructions, which may be the correct ones (if the jump is not taken) or the wrong ones (if it is taken).
To avoid stalling the pipeline, modern processors use branch prediction. A specialized component of the processor predicts whether the jump will be taken or not, and begins fetching and speculatively executing the instructions on the predicted path.
flowchart LR
subgraph Pipeline["5-stage pipeline"]
F["Fetch"] --> D["Decode"] --> E["Execute"] --> M["Memory"] --> W["Write-back"]
end
subgraph Branch_Predictor["Branch Predictor"]
BP["Predicts:jump taken?"]
end
BP -->|Prediction| F
style BP fill:#fff3e0,stroke:#ef6c00
If the prediction is correct, execution continues without penalty. If it is incorrect, the processor must discard all the instructions it had speculatively executed and load the correct ones. This penalty can be 10 to 20 clock cycles on modern processors, representing a significant performance loss.
The accuracy of modern predictors is astonishing: they exceed 97% on typical workloads. However, when they fail, the cost is high. This explains why programmers who write high-performance code sometimes try to avoid conditional jumps whenever possible, using techniques such as predicated execution or conditional move instructions (cmov).
3. The ternary operator: a conditional in an expression
The ternary operator, also called the conditional operator, is a compact way to write an if-else that returns a value. Its syntax, in languages like C, C++, Java, JavaScript, and Rust, is:
condition ? value_if_true : value_if_false
For example, in JavaScript:
const age = 18;
const message = age >= 18 ? "You are of legal age" : "You are underage";
This is equivalent to:
let message;
if (age >= 18) {
message = "You are of legal age";
} else {
message = "You are underage";
}
3.1. Why does the ternary operator exist?
The ternary operator exists because there are situations where we need to choose between two values conditionally within an expression. Without it, we would have to write a full if-else, which would be more verbose and, in some contexts, impossible (for example, when initializing a constant).
In Rust, if is an expression, so it can be used directly in an assignment:
let message = if age >= 18 { "You are of legal age" } else { "You are underage" };
In Python, there is a similar conditional expression (although with different syntax):
message = "You are of legal age" if age >= 18 else "You are underage"
3.2. The ternary operator at a low level: cmov and predication
Here is the deepest connection between the ternary operator and computer architecture. When a compiler encounters a ternary expression, it can often generate jump-free code using a conditional move instruction (cmov on x86, csel on ARM).
Instead of generating a conditional jump (which can fail prediction), the compiler generates code that calculates both values and then selects one of them based on the flags, without branching execution.
flowchart TD
subgraph "Traditional if-else"
A1["CMP x, 5"] --> B1["JG .L1"]
B1 --> C1["y = 0"]
C1 --> D1["JMP .L2"]
B1 --> E1[".L1: y = 1"]
E1 --> D1
end
subgraph "Ternary operator (optimized)"
A2["CMP x, 5"] --> B2["MOV eax, 0"]
B2 --> C2["MOV ebx, 1"]
C2 --> D2["CMOVG eax, ebx(if x > 5, eax = ebx)"]
D2 --> E2["y = eax"]
end
style A1 fill:#e3f2fd,stroke:#1565c0
style A2 fill:#e8f5e9,stroke:#2e7d32
In the cmov version, there are no jumps. The processor executes instructions linearly, and the value selection is performed in the execute stage without breaking the pipeline. This makes the ternary operator more efficient in terms of performance when the compiler can optimize it to a cmov instruction.
However, there is an important caveat: if the values in the ternary expression have side effects (such as function calls that modify state), the compiler cannot use cmov, because both sides would always be evaluated. In those cases, the ternary operator is compiled as a traditional if-else with jumps.
4. Examples in different languages
Let us see how a simple conditional is written in several programming languages and how each one behaves.
4.1. C
int x = 10;
if (x > 5) {
printf("x is greater than 5\n");
} else {
printf("x is less than or equal to 5\n");
}
In C, if evaluates an integer expression: any non-zero value is considered true. The compiler generates a CMP instruction followed by a conditional jump.
4.2. Java
int x = 10;
if (x > 5) {
System.out.println("x is greater than 5");
} else {
System.out.println("x is less than or equal to 5");
}
Java compiles to bytecode for the Java Virtual Machine (JVM). The if translates into instructions like if_icmpgt (compare integers and jump if the first is greater than the second), which operate on an operand stack.
4.3. Python
x = 10
if x > 5:
print("x is greater than 5")
else:
print("x is less than or equal to 5")
Python compiles to bytecode for the CPython virtual machine. The if generates instructions like COMPARE_OP and POP_JUMP_IF_FALSE, which also operate on a stack.
4.4. JavaScript
const x = 10;
if (x > 5) {
console.log("x is greater than 5");
} else {
console.log("x is less than or equal to 5");
}
JavaScript, being interpreted by engines like V8, compiles code to bytecode and then to native machine code via JIT compilation. Conditionals translate into comparisons and conditional jumps.
4.5. Rust
let x = 10;
if x > 5 {
println!("x is greater than 5");
} else {
println!("x is less than or equal to 5");
}
In Rust, if is an expression, meaning it returns a value. This allows using it in assignments:
let message = if x > 5 { "greater" } else { "less than or equal" };
The Rust compiler generates optimized assembly code, often using cmov or predication to avoid unnecessary jumps.
5. Complete diagram: from source code to hardware
The following diagram summarizes the complete journey of a conditional, from source code to execution in the processor.
flowchart TD
A["Source code:
if (x > 5)"] --> B["Compiler / Interpreter"]
B --> C{"Compiled or
interpreted language?"}
C -->|"Compiled (C, Rust)"| D["Assembly code:
CMP x, 5
JG .L1"]
C -->|"Bytecode (Java, Python)"| E["Bytecode:
COMPARE_OP
POP_JUMP_IF_FALSE"]
C -->|"JIT (JavaScript)"| F["JIT compilation
to machine code"]
D --> G["Assembler
to machine code"]
E --> H["Virtual Machine
interprets bytecode"]
F --> G
G --> I["Processor:
Fetch, Decode, Execute"]
H --> I
I --> J["Branch Predictor:
predicts the jump"]
J --> K{"Prediction correct?"}
K -->|Yes| L["Execution continues
without penalty"]
K -->|No| M["Discard speculative
instructions
Penalty: 10-20 cycles"]
M --> N["Load correct path"]
L --> O["Final result"]
N --> O
style A fill:#e3f2fd,stroke:#1565c0
style J fill:#fff3e0,stroke:#ef6c00
style M fill:#fce4ec,stroke:#c62828
6. Conclusion
The conditional is much more than a keyword in a programming language. It is the expression of one of the most fundamental operations in computing: the ability to make decisions. From the processor's condition flags to the branch predictors that try to guess the future, every conditional is a small battle between predictability and uncertainty.
The ternary operator, far from being a mere syntactic shortcut, is a window into the optimizations that modern compilers perform to avoid the costs of jumps. When the compiler can turn an if-else into a cmov instruction, it is eliminating a branch and keeping the processor pipeline flowing without interruptions.
Understanding these mechanisms is not necessary to write code that works. But it is essential to write code that works well. And to appreciate the incredible complexity hidden behind a simple if statement.
7. References
- Hennessy, J. L., Patterson, D. A., & Kozyrakis, C. (2025). Computer architecture: A quantitative approach (7th ed.). Morgan Kaufmann.
- Mittal, S. (2018). A survey of techniques for dynamic branch prediction. Concurrency and Computation: Practice and Experience, 30(1), e4707. https://doi.org/10.1002/cpe.4707
- ARM. (2023). Arm A-profile A32/T32 instruction set architecture. Arm Developer. https://developer.arm.com
- ECMA International. (2025). ECMAScript 2025 language specification (15th ed.). https://www.ecma-international.org
- Python Software Foundation. (2026). The Python language reference (Version 3.13). https://docs.python.org/3/reference/
- Oracle. (2026). The Java Virtual Machine specification. https://docs.oracle.com/javase/specs/
- Rust Team. (2026). The Rust programming language. https://doc.rust-lang.org/book/
- Stallman, R. M., & GCC Developer Community. (2025). GNU Compiler Collection (GCC) internals: Conditional execution. Free Software Foundation. https://gcc.gnu.org/onlinedocs/gccint/
Loading reactions...
Comments (0)
Loading session...
No comments yet. Be the first to comment.