---
title: 'Optimization Levels'
source: 'https://academia.sh/en/courses/c-systems-programming/optimization-levels'
course: 'Systems Programming with C'
language: en
updated: '2026-09-26T03:02:44+00:00'
license: 'CC BY-SA 4.0'
objectives:
    - 'Shows, through a model, when the as-if rule allows a write to be deleted'
    - 'Explains why a write marked volatile is never deleted'
    - "Collects the course's fifteen changes into a table by class"
---

# Optimization Levels

Uses the model of an optimizer deleting an unread write to cover the as-if rule, the guarantee volatile provides, and a closing tally of the course's fifteen lessons.

The previous lesson closed by noting that the value a debugger sees doesn't always match
what the source wrote: a variable may live in a register, or may have been removed entirely.
This lesson takes up where that difference comes from. An optimizer can remove a write that
doesn't change observable behavior — a write it removes never reaches any reader. The course
has measured this with a single question throughout: who sees this change? For optimization,
the answer reaches **unobserved** by a path different from the other fourteen changes: there
is no copy here and no intermediary. The write happens, but before any read reaches it,
another write lands on top of it.

## Write Overwritten Before Any Read

Think of a flow in three steps: a location is written with `1`, the same location is
written with `2`, and then that location is read somewhere. If the first write's effect was
never read before the second write arrived, then by the time the second write lands, every
trace of the first write in that location is gone. No party observing the program's
behavior — not output written to a file, not a value sent to another process, not shared
state protected by a lock — can tell whether the first write ever happened at all. The
program could have skipped the first write entirely and the observed result would not
change.

**PI10.** An optimizer model can remove the first of two consecutive writes to the same
location once it detects that no read occurred between them; whether it removes it or not,
the observable result is the same.

The measurement uses the same record format as the course's core:

```c
// observe.h — a record that classifies whom a change reaches
#ifndef OBSERVE_H
#define OBSERVE_H

enum Class { COPY, SHARED, HANDOFF, UNOBSERVED };

/* Records one change: seen is whether the side that did not make the
   change can read it (1 or 0). */
void observe(const char *change, enum Class cls, int seen);

/* Prints the count per class and the reached / not reached totals. */
void observe_summary(void);

#endif
```

```c
// observe.c — the counts live only in this unit
#include <stdio.h>
#include "observe.h"

static const char *const names[] = { "copy", "shared", "handoff", "unobserved" };
static int counts[4];
static int reached = 0;
static int not_reached = 0;

void observe(const char *change, enum Class cls, int seen) {
    /* shared and handoff reach, copy and unobserved do not */
    int expected = (cls == SHARED || cls == HANDOFF);
    counts[cls]++;
    if (seen) {
        reached++;
    } else {
        not_reached++;
    }
    printf("%-34s %-8s %s%s\n", change, seen ? "saw" : "missed",
           names[cls], seen == expected ? "" : "  MISMATCH");
}

void observe_summary(void) {
    printf("classes: copy %d, shared %d, handoff %d, unobserved %d\n",
           counts[COPY], counts[SHARED], counts[HANDOFF], counts[UNOBSERVED]);
    printf("reached %d, not reached %d\n", reached, not_reached);
}
```

The excerpt below is taken from the course's core, and it's this lesson's own excerpt — its
names and body do not change across the course:

```c
// optimization_measurement.c — the optimization excerpt from who_sees.c, unchanged
#include <stdio.h>
#include "observe.h"

struct Step {
    char op;
    int value;
};
static void optimization(void) {
    struct Step flow[] = { { 'w', 1 }, { 'w', 2 }, { 'r', 0 } };
    int readers = 0;
    int last_write = -1;
    for (int i = 0; i < 3; i++) {
        if (flow[i].op == 'w') {
            last_write = i;
        } else if (last_write == 0) {
            readers++;
        }
    }
    observe("write overwritten before any read", UNOBSERVED, readers > 0);
}

int main(void) {
    optimization();
    observe_summary();
    return 0;
}
```

```text
write overwritten before any read  missed   unobserved
classes: copy 0, shared 0, handoff 0, unobserved 1
reached 0, not reached 1
```

The `flow` array carries three steps: `'w'` is a write, `'r'` is a read. The loop tracks,
at each read step, which write was most recently in effect (`last_write`). When the read
arrives, if `last_write` still points at the **zeroth** write (`last_write == 0`), that read
counts as having seen the first write's value, and `readers` increases. In this model
`flow` holds, in order, `w=1`, `w=2`, `r`: the second step overwrites the first by moving
`last_write` to `1`; by the time the read at the third step arrives, `last_write` is no
longer `0`, it's `1`. No read ever sees the first write — `readers` stays at zero. The call
to `observe` records this with class `UNOBSERVED` and passes `readers > 0` (that is, `0`,
false) as the `seen` field.

This is the model for why an optimizer **can** delete an unread write: even if the first
write is never made at all, the `last_write` sequence — and therefore the `readers` count —
stays the same. If the program contained a single write instead of two — writing `2`
directly — the observable result (what the third step reads) would not change. The model
places `write overwritten before any read` in the `unobserved` class for exactly this
reason: a change was made, but it never reached any reader.

## The As-if Rule

An optimizer's right to delete a write isn't arbitrary; it rests on a single condition: the
**as-if rule**. This rule defines a program's run-time behavior in terms of a set of
**observable behavior**, and as long as the optimizer preserves that set, it may change how
the program executes step by step in any way it likes — it only has to look as if it ran in
source order, not actually run that way. Observable behavior is bounded by three things: the
order and content of input/output operations, accesses to objects marked `volatile`, and the
program's termination state. In the model above, the first write falls under none of these:
it isn't written to a file, it isn't `volatile`, and it doesn't affect how the program
terminates. So removing it does not violate the as-if rule.

**PI11.** A write can only be overwritten before being read if it falls into **none** of
the parts of observable behavior; the moment it falls into one, that right disappears.

The as-if rule doesn't conflict with the core's other fourteen measurements — it completes
them. The variable the child changes after `fork` (copy class) isn't subject to the as-if
rule at all — it's about the existence of two separate memories, not about write order
within a single process. The flag the handler raises (handoff class) enters the as-if rule
from the opposite direction: because it's marked `volatile sig_atomic_t`, the optimizer can
never touch that write — the next section works through this in detail.

## The Guarantee Volatile Provides

When the `volatile` keyword is applied to a variable, it tells the compiler that every read
and write to that variable counts as part of observable behavior. The consequence is direct:
a `volatile`-marked write cannot be deleted even if no read follows it; a `volatile`-marked
read cannot be skipped even if its result is used nowhere. The `flag` variable in the
signals lesson was declared `volatile sig_atomic_t` for exactly this reason: if the
optimizer could delete the write the handler makes to that flag on the grounds that "nobody
is reading it yet," before the main flow reads it, the handoff would never happen at all.

In the model's own terms: if the position in the `flow` array were `volatile`, the first
write would still be overwritten before being read, but it could not be deleted, because the
write itself would count as part of observable behavior. This doesn't change the record's
class — by the time the second write arrives, no read has still seen the first write's
value, and the record is still `unobserved`. What changes is only the optimizer's right: the
write stays in the program; it just never reaches a reader. This lesson doesn't measure
that with separate code; `volatile`'s effect shows up in the code an optimizer produces, and
a model in source text cannot print that code. This lesson's `main` function calls
`optimization` and then `observe_summary`; the output shows the one record marked `missed`
falling into the `unobserved` class.

The guarantee `volatile` provides is narrow, and it should not be stretched further. On its
own it does not provide ordering between threads, does not provide atomicity, and does not
establish a memory barrier — the lock and `join` from the POSIX Threads lesson are still
required. All `volatile` gives is the guarantee that accesses to that object will not be
deleted by the optimizer and that their order will be preserved; this is exactly why the
only form of sharing between a signal handler and the main flow is `volatile sig_atomic_t`.

## The Bounding Measurement

The model decides whether a write can be deleted by looking at a single rule — whether
observable behavior is preserved. A real optimizer makes this decision with much broader
information: whether a variable's address has been taken, whether it's visible from another
translation unit, whether it could be aliased through a pointer. This lesson does not write
an optimization level as a flag, and does not print compiler output, generated code, or a
measurement's duration — because none of these can be shown by a model in source text; they
are a real tool's real decision. The model shows only the rule itself: which writes can be
deleted, and which cannot. If a write's address is held by another function, that function
can read the location when called back; in that case the write is now part of observable
behavior and cannot be silently deleted the way the model shows. The model not showing this
is not a gap, it's a boundary: the excerpt only handles consecutive writes within a single
flow, not cross-function visibility.

## Summary

- `write overwritten before any read` (model): if two consecutive writes land on one
  location and the first is never read before the second arrives, no trace of the first
  remains.
- The as-if rule says an optimizer may delete a write only if doing so will not change
  observable behavior; observable behavior is bounded by input/output, `volatile` accesses,
  and termination state.
- `volatile` counts every access to an object as part of observable behavior; that's why a
  `volatile`-marked write is never deleted, even if it's never read.
- The `volatile sig_atomic_t` from the signals lesson relied on exactly this guarantee: the
  flag a handler raises cannot be deleted by the optimizer even while the main flow has not
  yet read it.
- The class this lesson measures is **unobserved**: a write happens but never reaches any
  reader — it doesn't stay on the writer's own side (copy), and it doesn't pass through an
  intermediary and get seen once (handoff); it gets overwritten and disappears.
- The model shows not a real optimizer's decision but the rule that decision rests on;
  address-taking and cross-function visibility are outside the model's scope.

## Course Wrap-Up

Across fifteen lessons, this course asked a single question: **who sees this change?** This
lesson answers that question for the last time, and the answer reaches unobserved by a path
different from the other fourteen — there's a write, no reader, because the next write
arrived before any read ever did. The table below collects all fifteen changes:

| Lesson | Measured change | Who sees it | Bounding measurement |
|---|---|---|---|
| File Descriptors | The offset shared by a descriptor duplicated with `dup` | shared | The descriptor number does not say what is shared; the sharing is measured only through offset behavior |
| Process Management | The variable the child changes after `fork` | copy | The measurement does not depend on timing; it does not measure a race, it measures that the two copies are independent |
| Signals | The flag the handler raises | handoff | The same signal arriving again while blocked is delivered only once when unblocked |
| Inter-Process Communication | The value written to the pipe | handoff | A pipe is a stream, it carries no message boundary; a boundary is established with a separate length prefix |
| POSIX Threads | The counter the thread writes under the lock | shared | Only the final value is verified; intermediate states and write order are not measured |
| Linked Structures | The node changed through an alias | shared | A pointer's sharing is not visible at the source; whether two heads point at the same node is measured by equality |
| Ring Buffers and Queues | The item put in the ring | handoff | The read function's signature doesn't say whether the value stays in the structure; this is measured with two reads in a row |
| Hash Tables | The key changed after insertion | copy | The insert function's signature doesn't say whether the key is copied; if the key were held by pointer, the class would be shared |
| Object-Like Design | A deposit made by one user | shared | The shape of the interface (opaque type, function pointers, plain functions) does not determine sharing; only pointer equality does |
| Resource Cleanup Idioms | The buffer exited without being flushed | unobserved | Unobserved means reaching nowhere; this is verified with `== NULL` checks, without printing the value |
| Build Systems | The header with a dependency that was never written | copy | The model only sees the dependency that was written; it has no way of knowing on its own which header is actually read |
| Building Libraries | An update to a statically linked library | copy | The model only measures which value is reached; if the interface changes, the pointer-based guarantee is lost, and that case is not run |
| Testing Frameworks | The change made by an isolated test | copy | Process isolation cuts off memory change, not filesystem change |
| Using a Debugger | The intermediate value between two stops | unobserved | A breakpoint and a watchpoint observe a running process; a core dump reads one that has already stopped; no dump is produced |
| Optimization Levels | A write overwritten before any read | unobserved | The model only handles consecutive writes within a single flow; address-taking is out of scope |

**5 copy · 4 shared · 3 handoff · 3 unobserved.**

This distribution matches the distribution of the core's fifteen observations exactly — not
an unexpected coincidence, because the core carries exactly one observation per lesson, and
each lesson takes its own excerpt from that observation. Seven changes reached the other
side, eight did not; the ones that reached passed through an intermediary (handoff) or a
shared object (shared); the ones that didn't either stayed on the writer's own side (copy)
or never landed anywhere at all (unobserved). This distinction held from the start of the
course: a change **having been made** does not guarantee that it was **seen**.

The same question was asked again at different levels throughout the course — between two
processes' memory, between a handler and the main flow, in the memory two threads share, in
the trace one build unit leaves for another, and finally in a single write an optimizer
deletes. At every level, the answer fell into one of the same four classes; the number of
classes never changed, only which mechanism fell into which class changed.

The next course (C++ Fundamentals and Object Design) covers classes and resource
management; it will establish its own measurement axis in its own first lesson.
