Skip to content
academia.sh

Lesson 15 / 15

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.

Published Updated

In this lesson

  • 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
Contents

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:

// 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
// 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:

// 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;
}
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.

To keep your progress and take notes, Log in

My notes

Log in to take notes.

Start typing to search.

↑↓ ↵ Esc navigate · open · close