Languages and runtimes
Your language is a machine too — garbage collector, JIT and all — and it has a cost model of its own.
Your language is not a neutral pipe to the machine — it is a machine of its own, with a memory manager, possibly a JIT, and opinions. You do not need to write compilers to be a professional; you do need to know what the runtime is doing on your behalf, because its costs show up on your profiles wearing your function names.
What a compiler does, in one paragraph
Front end: read text into a tree (parse), check the tree makes sense (types, scopes). Back end: lower the tree to instructions, then optimise — inline small functions, hoist loop-invariant work, delete code whose result is unused. Two practical corollaries: compiler error messages are the front end being honest with you, worth reading slowly; and optimisers are why microbenchmarks lie (previous chapter) and why “cleverly hand-optimised” source often ties with the straightforward version — the back end already did it.
Garbage collection — the deal you signed
Managed languages trade manual freeing for a collector that finds unreachable objects and reclaims them. Modern collectors are generational — most objects die young, so the nursery is swept often and cheaply while old survivors are visited rarely — and pauses that once stopped the world for seconds are now typically milliseconds. The professional concern has therefore moved: it is rarely “GC pauses” and usually allocation rate. A hot path that allocates a million short-lived objects a second keeps the collector permanently busy, and the cost is diffuse — a few percent everywhere, a warmer cache nowhere. The fixes are the memory chapter’s: reuse buffers, prefer flat arrays of values, hoist allocation out of loops. Rust’s ownership model and reference-counting runtimes strike the same deal with different coins; the invariant across all of them is that someone must know when memory is done, and that someone is on your profile.
Types, and the boundary tax
Static versus dynamic typing is an engineering dial, not a religion. Static types are executable documentation and a refactoring safety net that pays off superlinearly with team size and codebase age; dynamic types buy iteration speed at small scale — which is why the industry pattern is gradual typing bolted onto successful dynamic codebases (TypeScript, Python type hints), adopted exactly when the maintenance bill arrives. Separately, know the FFI boundary tax: crossing between runtimes (Python↔C, JS↔native) costs conversion and bookkeeping per call. The fast pattern everywhere is coarse crossings — hand NumPy the whole array, not a million floats one call at a time. It is the syscall lesson and the network lesson a third time: batch at boundaries.
The one exercise worth its weekend
Build a tiny interpreter once — a calculator with variables is enough, the linked book if you want the full journey. Not for the artefact: because afterwards, stack traces, scoping rules, “undefined is not a function”, and what a REPL even is all stop being folklore. It is the single densest exercise per hour in this pathway.
Where it bites
- Allocation inside the hot loop — the collector charges everyone a little, so nobody suspects it.
- Chatty FFI: a thousand tiny calls across a runtime boundary instead of one batched call.
- Benchmarking before JIT warm-up, or comparing a warmed run against a cold one.
- Untyped success: the 100k-line dynamic codebase where every refactor is an act of faith — adopt gradual types before that line, not after.