Lesson 1 of 9

Lesson 1 — What does the CPU actually execute?

Estimated reading
15–20 min
Exercises / exploration
10–15 min additional
Activity type
Recorded native evidence

Native experiments use recorded evidence from the stated environment. Native code does not execute here; browser models are separate.

Available here: interactive model, complete text, code and worked answers.

OS-01 · From code to CPU work · Lesson 1 of 9

You can already read a loop and call a function. That is enough to start. You do not need C, assembly, or operating-system experience: we introduce the small pieces used here.

By the end, you should be able to distinguish source code from machine instructions, trace a loop through changing state, and explain why a source-level operation count does not establish execution time.

Reading estimate: 15–20 minutes. Experiment: 10–15 minutes. These are editorial estimates, not learner-tested timings. You can follow the entire explanation without running anything.

Why we begin here

When a program is unexpectedly slow—or a benchmark looks impossibly fast—you need to establish what work it actually performed. Otherwise it is easy to explain an observation using a mechanism that was never involved.

This lesson builds that first habit with a small enough program to inspect completely. We will connect the code you write to the instructions that execute it. Later lessons add memory access, waiting, concurrency, and communication; AI systems combine those mechanisms into a larger request path. You are learning how to choose and test an explanation, starting with one manageable example.

For the overall journey, read Why learn what happens beneath your code?. You can still enter this lesson directly or skip ahead.

A loop that does not loop

Suppose a function starts a counter at zero, adds one ten million times, and returns the result. How much work does the computer do?

“Ten million additions” is a reasonable description of the source. It might be completely wrong about the executable.

Consider this function, from work.c:

C · native Linux example (read-only)
uint64_t count_steps(uint64_t n) {
    uint64_t total = 0;
    for (uint64_t i = 0; i < n; ++i) {
        total += 1;
    }
    return total;
}

Here is all the C vocabulary you need. uint64_t is an unsigned 64-bit integer type: it represents whole numbers from zero through 2^64 − 1 on this target. n is the input. total holds the answer. The loop starts i at zero, continues while i < n, and increments i with ++i after each iteration. total += 1 means add one to total. return supplies the answer to the caller.

For n = 3, the source describes this state:

PointitotalNext decision
Before the first iteration000 < 3: enter
After one iteration111 < 3: enter
After two iterations222 < 3: enter
After three iterations333 < 3: false; return 3

Now ask a different question: could the function simply return n?

Yes. Nothing outside this function observes its intermediate counter values. For this function, returning n preserves the result, including when n is zero. That observation is the key to the experiment.

1. Source code is a description, not a hardware transcript

A CPU core is hardware that carries out machine instructions. An instruction can request operations such as arithmetic, reading data, or selecting where execution continues. Instructions have binary encodings understood by a particular instruction set, such as x86-64 or AArch64. The processor does not directly interpret the words for and return in this C file.

A compiler translates source into a form suitable for a target machine. It can transform the implementation while preserving the behaviour required by the language. With GCC, building a C executable involves preprocessing, compilation, assembly, and linking. You do not need the details of all four stages yet. The useful distinction is that readable assembly is a textual representation of machine instructions, while an executable contains machine code and the information needed to load it. Linking connects compiled pieces, including needed library code. [1]

Compiler optimization matters because two builds of the same source may contain very different instructions. “I wrote a loop” does not prove “the executable repeats those operations.” [2]

This distinction exists in other languages too, although the route differs. In CPython, source is compiled into Python bytecode, an intermediate representation executed by the interpreter. Python bytecode is not your processor's native instruction set. The interpreter and native libraries themselves ultimately execute machine instructions. This is why timing a Python loop is not a clean measurement of the cost of one native integer addition. [3]

2. A small execution model

Three terms make it easier to talk about instruction execution:

  • Register: a small named storage location in the processor's architectural state, used for values such as operands and addresses. A source variable need not have one permanent matching register.
  • Memory address: a number used to identify a location from which data can be read or to which it can be written. We will later distinguish virtual addresses from physical memory.
  • Branch: an instruction that can change where execution continues. A conditional branch chooses based on a condition, such as the result of a comparison.

Imagine an implementation that really performs our loop. Its control flow looks like this:

Choose n from 0 to 5, then use Step to follow the operations, Play/Pause to follow automatically, or Reset to restore n = 3. Watch how the condition controls repetition: the highlighted operation has just happened, and the table separates its before and after states.

What makes the loop repeat?

The condition chooses the body or the exit. Follow one operation at a time.

yesnoback to conditioni = 0, total = 0i < n ?total += 1i += 1return total

Before initialization

Before and after the current operation · n = 3
Stateitotal
Before——
After——

Condition checks: 0. Total increments: 0. A completed trace checks n + 1 times and increments n times. With n = 0 it exits without entering the body. This source-level model is not native execution or timing; the compiler can eliminate the loop later in this lesson.

Read the arrows as possible next steps, not equal units of time. A true comparison leads through the two increments and back to the same condition. A false comparison exits. The body increments total exactly n times, but the condition is checked n + 1 times, including the final false check. With n = 0, the body never runs. This source-level conceptual trace is not native execution or timing, nor a claim that each box maps to one machine instruction or clock cycle. Later, compare it with the compiler's elimination of this loop.

A clock cycle is a tick of the processor's clock. It is not a promise to finish one source operation—or even exactly one instruction. Modern processors can overlap instruction work; dependencies and unavailable resources can also delay progress. Instruction count and clock frequency therefore do not, by themselves, determine time. [4]

For now, keep two questions separate: what instructions remain after compilation, and what happens when the processor executes them? We must answer the first before explaining the second.

3. Predict before running

We will compile the same files twice:

  • -O0: GCC with most optimizations disabled.
  • -O2: GCC with a set of optimizations enabled. Exact transformations depend on compiler version and target. [2]

For each build, try inputs of one million, ten million, and fifty million. Write a prediction:

  1. Will both builds return the same answer?
  2. Must a fifty-times-larger input take fifty times longer?
  3. What evidence would distinguish “the loop became faster” from “the loop disappeared”?

Do not change the function to make the result unused. Our program prints and checks the answer; it is not an empty benchmark whose work is irrelevant.

4. Explore the experiment

This lesson uses recorded native results, not live C execution. You can continue directly to the worked results in section 5 without installing anything. The commands below are an optional local route, not a prerequisite for understanding the lesson.

What the runner measures

The reference environment is Linux x86-64, GCC 13.3.0, Python 3.12.14. The Python script is the experiment coordinator: it invokes the compiler, runs the executables, and collects their output. The measured function is compiled C. You do not need to learn the coordinator's implementation to complete this lesson.

The native program bounds input at 100 million. Its timer surrounds the function call, so compilation, process startup, and printing are outside the reported interval. The call boundary and timer overhead still matter. The monotonic clock measures elapsed time without wall-clock jumps; it does not isolate CPU execution from interruptions or scheduling. [5]

Optional: reproduce locally in a Linux terminal

Use this route only if you want to run the repository example yourself. It requires Python 3 and GCC on your command path. Native Windows and macOS execution have not been validated. A compatible Linux environment is required; no local setup is necessary for the reading-only route.

Save main.c, work.c, and run.py from the lesson files into one directory. Open a Linux terminal in that directory and run:

Shell · Linux command (read-only)
python3 run.py

The script uses no downloaded packages. It builds in a temporary directory, prints the assembly, checks answers and invalid inputs, and reports five samples for each build and input. Each subprocess has a 15-second deadline; temporary build files are removed when the script exits. Ctrl+C stops the script. Running again starts fresh.

For a manual look at the generated assembly:

Shell · Linux command (read-only)
gcc -std=c11 -O0 -S work.c -o work-O0.s
gcc -std=c11 -O2 -S work.c -o work-O2.s

-S asks GCC to stop after emitting assembly; these commands do not create runnable programs. [1] The manual .s files remain for you to inspect; deleting those two files resets that step.

5. What happened in the validated run?

Both builds returned the requested count. Their median elapsed times were:

Input n-O0-O2
1,000,0000.780 ms0.581 microseconds
10,000,0009.259 ms0.551 microseconds
50,000,00046.196 ms0.671 microseconds

These are actual observations from a shared Linux environment, not performance promises. The raw samples and compiler output are in validation-output.txt. Small optimized timings are dominated by measurement and call overhead; they do not tell us the intrinsic cost of the move instruction below. Do not turn this table into a claim of a precise speedup factor.

The decisive evidence is the generated function. In the optimized x86-64 build, its relevant instructions were:

Assembly · recorded compiler output
movq %rdi, %rax
ret

The full output also includes endbr64, a control-flow protection instruction unrelated to counting. We omit it above to focus on the result transfer.

In this platform's calling convention, the first integer argument arrives in register rdi, and an integer result is returned in rax. This is a convention for this platform, not a universal rule. In the assembly syntax printed here, movq copies a 64-bit value from the first operand into the second; % marks a register. ret returns control to the caller. The argument/result register roles follow the x86-64 System V ABI. [6]

So the compiler-generated function transfers n to the result register and returns. There is no counting loop left. It works for runtime inputs, not just a number known when compiling.

In the unoptimized output you can find addq instructions for the counters, cmpq for comparison, and jb for a conditional jump back into the loop. The complete listing contains other bookkeeping; you do not need to decode every line to establish that repeated work remains.

If your compiler emits different code, inspect that code before explaining its timing. The lesson is a method of checking what happened, not a requirement that every compiler produce these two instructions.

6. What this establishes—and what it does not

We have evidence that optimization removed this loop while preserving its answer. We have not measured branch prediction, cache performance, or the speed of C versus Python. Those need different experiments.

Nor does this mean a compiler can remove any expensive loop. Our function's only externally visible result is a value the compiler can derive without iteration. Reading unknown array elements, performing required I/O, or maintaining observable effects changes the problem.

Adding volatile to force accesses would also change the experiment. It is not a general performance fix, a portable synchronization mechanism, or a way to make a benchmark representative.

The practical habit is useful beyond this toy example. When a small benchmark seems impossibly fast, first ask whether it performed the intended work. When a service seems slow, source-level complexity is only a starting hypothesis: data access, dependencies, waiting, and generated code still need investigation.

7. Check your understanding

A. Trace the empty case

For n = 0, how many times does the loop body run, and what should both executables return?

Hint

The condition is tested before entering the body.

Answer and explanation

Zero times, returning zero. The initial condition 0 < 0 is false. Returning n preserves this boundary case too.

B. Diagnose a tempting explanation

A colleague says, “The optimized version must be faster because the CPU predicts the loop branch perfectly.” What evidence would you request, and what does our result show?

Hint

Before explaining the cost of a branch, check whether that branch exists.

Answer and explanation

Request the generated code for the timed function. In our optimized build there is no counting-loop branch to predict. The observed transformation supports loop elimination, not a branch-prediction explanation.

C. Transfer the idea

You replace the body with “add the next value from an array supplied by the caller.” Can the compiler always return n instead? What two arrays would disprove that replacement?

Hint

Keep the length equal and change the values.

Answer and explanation

No. [1, 1, 1] sums to 3, but [2, 2, 2] sums to 6 despite the same length. The output depends on data, not merely length. Other optimizations may still be valid; this counterexample only disproves replacing the general sum with its length.

D. Reject a bad measurement claim

The optimized timer printed a value close to zero. Does that mean the processor completed the function in zero time?

Answer and explanation

No. Resolution, timer overhead, rounding, and variation limit what such a small interval tells us. Report that the function was too short for this harness to time meaningfully in isolation. Inspecting its instructions remains informative.

You are ready to move on when you can explain the disappearance of the loop without appealing to magic CPU speed, and identify one observation that would invalidate your explanation.

Where this leads

Continue to Where does an instruction keep its work? and follow the complete execution section. The AI preview connects these habits to diagnosing a service.

The deeper predictable-versus-shuffled branch experiment remains planned; the dependency lesson uses an explicitly labelled conceptual model. You can explore the full curriculum freely.

Sources and scope

Checked 15 October 2026. These references support specific mechanisms; the recorded experiment supports the claims about this particular build.

  1. GCC: overall options (opens in a new tab) — compilation stages and -S.

  2. GCC: optimization options (opens in a new tab) — optimization levels and target/version-dependent passes. Online documentation can differ from installed GCC; our observed compiler version is recorded separately.

  3. Python glossary: bytecode (opens in a new tab) — CPython's intermediate representation, distinct from native instructions.

  4. Intel VTune: CPU metrics reference (opens in a new tab) — instruction/cycle metrics and resource/dependency limits. These details concern Intel processors, not a universal processor timing specification.

  5. Linux manual: clock_gettime (opens in a new tab) — monotonic elapsed-time measurement.

  6. x86-64 psABI: low-level system information (opens in a new tab) — integer parameter and return register conventions.

The reference experiment was author-checked; independent technical review remains pending. The recorded compiler version and raw output establish the scope of these observations.

Where did we lose you?

Tell us which explanation needs another step or which result surprised you. We'll see which lesson this is about.

Sign in to send feedback