02 — The Lab Notebook¶
This is the single most important C-specific practice in this document. If you drop everything else in this discipline layer and only keep one habit, keep this one.
Predict before you measure. Every time. Write the prediction down FIRST. Then measure. Then write the delta.
Why this matters more in C than in Python¶
In Python, your performance intuitions are usually not-quite-right in obvious ways: “this loop is slow, this list comp is faster, this numpy call is fastest.” You are wrong by 3x maybe. In C, you are wrong by 10x, 100x, or in the wrong direction entirely, and the causes are non-obvious:
The compiler autovectorized a naive loop and beat your “clever” version
Your
-O0build made a benchmark look 40x slower than the truthA cache miss you didn’t model dominated a 20-line function
ASan overhead turned a 200ns operation into 3µs and you drew the wrong conclusion
A conditional branch got predicted perfectly on your test data and lied to you about production
memcpywas faster than your handwritten SIMD because glibc knows things you don’tThe struct padding you didn’t notice cost you 30% cache-line efficiency
You cannot fix intuitions you never surface. The notebook forces you to surface them. Every wrong prediction, written down and dated, is a rung on the ladder from “ML engineer who dabbled in C” to “engineer who knows what the machine actually does.”
The template¶
One entry per measurement. Not one per project. Not one per week. One per measurement.
### YYYY-MM-DD · <one-line title>
**Hypothesis:** what I think the machine is doing
**Prediction:** what number I expect (with units, and rough)
**Method:** how I measured (toolchain, flags, input size, repeats)
**Result:** the actual number(s), with variance
**Delta:** how wrong I was, and in which direction
**Lesson:** what I now believe about the machine
Six fields. Never fewer. The Delta and Lesson fields are the point — if you skip them because “the result was as expected,” you have failed to close the loop, and your intuitions do not update.
Three sample entries (plausible for early months)¶
2026-08-12 · Bubble sort 10k ints, -O0 vs -O2¶
Hypothesis: bubble sort is O(n²) so 10k ints should be ~100M ops → around 500ms on my M1.
Prediction: 500ms at -O0, maybe 200ms at -O2.
Method: clang -O0 and clang -O2, 10k random ints, wall time from clock_gettime(CLOCK_MONOTONIC), 5 runs, median.
Result: -O0 = 380ms. -O2 = 120ms.
Delta: -O0 faster than predicted (missing the pipelined-branch effect on modern CPUs). -O2 also faster than I thought — inspecting the assembly, clang autovectorized the swap comparison despite the data dependency, which I did not think was possible.
Lesson: modern compilers vectorize more than my mental model expects, even at O2 without -march=native. Never claim “this loop is scalar” from source; check the assembly.
2026-09-03 · malloc vs stack for 1KB buffers in a hot loop¶
Hypothesis: malloc has to hit the allocator, so 1M iterations of malloc+free will be much slower than a stack buffer.
Prediction: malloc version 100x slower.
Method: -O2, 1M iterations, each allocating 1024 bytes and writing one byte to page it in.
Result: malloc 8x slower, not 100x.
Delta: off by an order of magnitude. glibc’s ptmalloc has a thread-local fast path for small allocations that is much cheaper than I thought.
Lesson: “malloc is slow” is a slogan, not a measurement. For hot paths, always time it — the answer depends on allocator, size, and threading, not on folklore.
2026-10-18 · Struct padding: array-of-structs vs struct-of-arrays for a 1M-element sum¶
Hypothesis: SoA will win because of contiguous access and vectorization; AoS will pay cache-line waste.
Prediction: SoA 2x faster.
Method: struct with int32 id; double value; char flag; in a 1M array, vs three parallel arrays. Sum the value field. -O2.
Result: SoA 3.4x faster.
Delta: correct direction, bigger magnitude than predicted. The AoS struct was 24 bytes after padding, so each cache line held ~2.6 useful doubles vs 8 for SoA. That accounts for the 3x cleanly.
Lesson: padding maths: sizeof(struct) is a real number you should compute at design time, not discover at benchmark time. When a double sits between an int32 and a char, you are paying for that geometry.
Where to keep the notebook¶
Three options, ranked by friction:
A private git repo with markdown files, one per week. Best long-term. You can grep it three years later. Zero-friction editing from any editor. Recommended.
Obsidian vault. Fine, has nice linking, but ties you to a tool. Fine if you already live in it.
A plain markdown file in the project. Acceptable for the first month; it will get unwieldy by month three.
Do not use a physical notebook. You will not go back and read a physical notebook. It looks romantic and produces nothing. The whole point is that the notebook is a searchable corpus of your own wrong predictions, and paper is not searchable.
The friction floor rule¶
Whatever tool you pick, the rule is: it must be openable in under 5 seconds from your terminal. If it takes longer, you will skip entries on the days you needed them most. Alias it. Bind it to a hotkey. Make it a nb shell function that opens the current week’s file. Friction kills habits; the notebook is a habit.
What “one measurement” means¶
You do not need an entry for every printf. You need an entry when you did something and the machine gave you a number, and you care about that number. Rough guide: 3–5 entries a week during learning-heavy months, 1–2 per week during project-heavy months. By M13 you should have 100+ entries. That corpus IS your seniority proof, even more than the portfolio.
Return to README.md · Next: 03_benchmark_hygiene.md