Published Friday, July 31, 2026 at 10:04 AM PT
Burbank · Friday, July 31, 2026 · 10:04 AM · 83°F, 54% humidity, wind 0 mph N (gusts 1), 29.40 inHg, UV 0, PM2.5 9
Programming is not a discipline. It is a religion built on spite, maintained by people too stubborn to admit that we’ve been doing this wrong for fifty years, and periodically reinvented by teenagers who read a Medium post and decided they’d invented a better way to allocate memory. The “source material” for this essay is a perfect specimen: a chaotic collection of technical documentation spanning the 1970s to the present, each author convinced their problem was unique, their solution elegant, their language finally the one that would fix everything. None of them were right. All of them were solving the same five problems their predecessors had already solved, poorly, a decade prior.
This is not a judgment. It is an observation about the nature of work in a domain where the foundational constraints—CPU cycles, memory, I/O latency—are immutable, but the abstractions we build atop them shift every few years like tectonic plates. We are programming in an endless Sisyphean loop: measure performance, discover timing is wrong, fix timing, break something else, blame the operating system, build a new language to replace the last one, repeat.
The evidence is right here in the fragments: C* programmers in 1993 struggling to time small code blocks accurately because OS I/O operations don’t show up in their measurements. Fifty-three years later, we are still having the same conversation about why profiling is hard and benchmarks lie. The lexicon changes—we have containers now instead of mainframes, GPUs instead of vector processors—but the fundamental problem is identical: you cannot trust the tools that measure your tools, and you must measure everything seven times to believe anything.
Programming is not science. It is not engineering. It is accounting: tracking resources that are perpetually inadequate, trading off one constraint against another, and lying to ourselves about progress because the honest answer—“we don’t really know what we’re doing”—is professionally unacceptable.
The authors of the C* Performance Guide (Thinking Machines Corporation, August 1993) were not dealing with an obscure edge case. They were confronting a central, unsolved problem of programming: you cannot know how fast your code is. Not in any meaningful way. Not without drowning yourself in methodological caveats that would make a statistician weep.
Their solution was precise: introduce a loop. Execute the code one hundred times. Measure the aggregate time. Normalize for startup costs by running the code with “widely varying subgrid sizes” and fitting a line through the data. It is reasonable. It is methodical. It is also a band-aid on a compound fracture.
The actual problem—the one they circle around but never quite name—is that the operating system is lying to you about time. When you call gettimeofday(), you get “wall clock” time, the time a human with a stopwatch would measure. But that is not the time your code spent executing. It includes the time the OS spent swapping pages, servicing interrupts, context-switching to other processes, and doing whatever mysterious I/O operations it decided were critical. Your code was not running. But it was still on the clock.
Thirty-three years later, this problem is worse. Modern CPUs have speculative execution, branch prediction, CPU frequency scaling, and cache hierarchies so deep they might as well be quantum. A memory access that hits L1 cache is three cycles. A memory access that misses L1, hits L2, is twelve cycles. A miss all the way to L3 is around forty cycles. A memory access that misses the entire cache hierarchy and goes to main RAM is three hundred cycles—or more if the DRAM rows need refreshing. The CPU may reorder your instructions, executing them out of sequence if it can prove the result will be the same. It may speculate on branch outcomes and throw away an entire pipeline of work if it guessed wrong. Your code’s actual runtime depends not just on what you wrote, but on what random other code ran immediately before it, what the memory access patterns were, whether the branch predictor’s history table has entries relevant to your branch, and whether the CPU decided to throttle down its clock because it hit a thermal target.
A modern benchmark that runs code in isolation gives you completely different results from the same code running in production with twelve other processes fighting for cache. The benchmark is not wrong—it is measuring something real, something authentic to the isolated scenario. But that scenario does not exist in production. Production is a chaos of competing demands, and your code’s actual performance is an emergent property of that chaos.
Profilers can measure it. They will give you different answers depending on which profiler you use. The answers will change between runs. A profiler that instruments every function call adds overhead that distorts the measurements. A sampling profiler that wakes up every millisecond and records what function is currently executing might miss entirely the fast code paths—they execute in microseconds, faster than the sampling rate. A hardware profiler that uses CPU performance counters gets as close to ground truth as you can get, but it measures the wrong level of abstraction—cache misses are real, but they are a symptom of memory access patterns, not the root cause of slowness.
The honest response—“your code’s performance is fundamentally unknowable in a way that matters”—is unacceptable. So we lie. We build better measurement tools. We measure more carefully. We introduce statistical filters and ignore outliers. We run benchmarks repeatedly and take the median. We warm up the JIT compiler before taking measurements. And still, none of it works. A benchmark that says your code is 50% faster is not necessarily wrong. It is also not necessarily right. It is, best case, correct within an error margin you didn’t publish and that probably doesn’t include the variance you’ll see in production.
The C* programmers knew this was garbage. They inserted the word “actually” into their guidance: you should actually use wall-clock times, not CPU times, because your measurements don’t include I/O. That “actually” is a capitulation. It is the sound of engineers conceding that the entire measurement apparatus is compromised, and you should at least measure the compromised thing honestly.
We are still in this exact position. Your production performance monitoring is lying to you. Your load testing is running in an environment that does not exist and will never exist. Your benchmark results are correct within a margin of error that is never disclosed. The Kafka cluster runs 10% slower in production because the network topology is different from your test environment. The database query that takes 50 milliseconds in the staging database takes 500 milliseconds in production because the production database has real data and a different distribution of queries. Programming has become accounting with precision that feels scientific but is not.
The source material includes a crisp taxonomy of memory-safety failures: stack exhaustion (too-deep recursion), heap exhaustion (ran out of memory), memory leaks (never freed what you allocated), null pointer dereference (dereferenced garbage). These are listed matter-of-factly, as though they are categories of weather. The document was written sometime in the 1990s or 2000s.
We are now in 2026. These are still the five things that break code. Not new kinds of things. Not more interesting things. The exact same things. We have added exactly zero categories in thirty-five years.
The document notes—correctly—that in some languages (it means C), heap exhaustion “must be checked for manually after each allocation.” This is presented as a limitation of C, a language defect. But C is still the dominant systems programming language. And we are still checking heap exhaustion manually. Or rather, we are not checking it, and then we wonder why production services crash with Out-of-Memory errors that we could have predicted if anyone had been checking anything at all. The Linux kernel ships with an OOM killer that terminates processes when memory runs out, because we have accepted that preventing exhaustion is too hard and we might as well choose victims. This is normal. This is how trillion-dollar infrastructure works.
The document also notes that null pointer dereference “will often cause an exception or program termination in most environments, but can cause corruption in operating system kernels or systems without memory protection.” This is true. It is also a statement of capitulation. We are building systems where dereferencing NULL is defined behavior in the kernel. The kernel is software. Software is written by people. People are not good at thinking about memory. The kernel crashes. The entire system goes down. This happens. You restart it. This is normal.
The Rust community has spent years arguing that they solved this. Rust prevents null pointer dereferences by making them syntactically impossible—you cannot dereference None, the language won’t let you. This is presented as progress. It is a workaround. The problem—human programmers allocate pointers and sometimes forget to initialize them—is solved by making the language refuse to compile code that forgot. This is good. It prevents an entire category of runtime failure. But it does so by pushing the problem earlier: instead of crashing at runtime, you fail at compile time. This is an improvement, genuinely, but it is an improvement in when failure happens, not in whether the failure is possible.
Memory safety in Rust is achieved through a system of borrow checking that tracks lifetimes at compile time. The compiler proves that references cannot outlive the data they point to. This is mathematically sound. It is also restrictive. Entire classes of algorithms are difficult to express in Rust because they require patterns that the borrow checker cannot prove are safe, even if they are. You write unsafe code. The unsafe code is now your responsibility—the language got out of the way and said “you’re on your own for this one.” The unsafe code has null pointer dereferences just like C. They are just surrounded by a neon sign that says “HERE IS WHERE BAD THINGS MIGHT HAPPEN.” This is progress. It is also an admission that we gave up on teaching people to think about memory correctly and instead built a language that thinks about it for them, with an escape hatch for when the language is not smart enough.
But Rust does not solve the real problem: distributed systems crash because one node ran out of memory and took the whole cluster down. Languages don’t protect you from that. Load balancers crash because someone wrote a memory leak in Go, a language with garbage collection and memory safety. The memory leak is still there; the garbage collector is just too slow to catch it before the OOM killer fires. The garbage collector itself might be the problem—a full GC pause that freezes the entire application for 500 milliseconds while the collector sweeps the heap. In a real-time system, a 500-millisecond pause is a disaster. In a web application serving thousands of requests, that pause means some requests timeout while waiting for the pause to finish.
We have not solved memory safety. We have introduced a spectrum of compromise positions, each one an admission that the previous compromise was insufficient. C: manual memory management, crashes if you fuck up. C++: RAII, smart pointers, deterministic cleanup, crashes slightly less often but you have to remember the rules. Java: garbage collection, no memory management, crashes only if something is really wrong, but now you have to manage GC pause times. Go: garbage collection with lower latency, crashes when GC can’t keep up or you have a genuine memory leak. Python: garbage collection plus reference counting, so slow that you rarely hit memory limits because the program can’t run fast enough to exhaust memory. Rust: compile-time borrow checking, still crashes but for different reasons, crashes at compile time instead of runtime.
None of these is a solution. All of these are different bets on what kind of failure you’re willing to tolerate. We are not making progress. We are just rearranging the furniture in a house that is structurally unsound.
The source material includes fragments from three different programming languages from three different eras, each one solving the problem of “how do you write programs” in a different way: SAIL (1970s, batch processing, Stanford AI Lab), TECO (1960s-80s, text editing, Digital Equipment Corporation), Lisp (1960s-present, functional programming, MIT).
SAIL had break tables. You set BREAKSET to define which characters would terminate your input scanning, and in what order. The input function would read characters and check each one against the break table, stopping when it hit a character that matched. It is a simple pattern: define delimiters declaratively, let the system handle the scanning. TECO had q-registers—variables that could hold text, numbers, or even vector indexing. They were the primitive storage mechanism for everything. Lisp had S-expressions and atoms and lists, all composing upward in an elegant recursive structure. An S-expression was either an atom or a list of S-expressions. Lists were how you represented code and data. Everything was uniformly a list.
They were all solving the same problem: how do you define, store, and manipulate data? The answer in 1970 was “break tables.” The answer in 1990 was “objects and methods.” The answer in 2010 was “functional reactive programming.” The answer in 2026 is “TypeScript generics and dependency injection frameworks with decorators.”
The problem has not changed. Data comes in from somewhere (a user, a file, a network socket). You need to parse it, extract meaning, transform it, and send it somewhere else (a screen, a database, another network socket). In SAIL, you did this with break tables and INPUT calls. You would set the break table, call INPUT, and get back a value that was terminated by one of the break characters. Today, you do this with middleware, decorators, type systems, and frameworks. A request comes in to an Express route handler decorated with @Post('/api/data'). The middleware deserializes JSON. The decorator validates the input against a schema. The function runs. The result is serialized back to JSON. The abstraction layers are thicker, but the process is identical.
The source material includes a section on “Script polyfills”—code that makes behavior consistent across different browsers by patching in missing features or fixing bugs. This is a direct, line-for-line parallel to what TECO programmers did in the 1980s: write macros to work around bugs in specific versions of the editor. You wanted the editor to do X, but the version you had on your machine didn’t support it, so you wrote a macro that made X happen anyway. Different era, same problem. Different language (JavaScript vs. TECO), same solution. It is called “progress” because we have faster computers and networks, but the intellectual work—“this browser has a bug, let me write code to fix it”—is identical. Chrome doesn’t implement the Web Audio API the same way as Firefox, so you write a polyfill that detects which browser you’re in and wraps the API appropriately. TECO didn’t support searching backward, so you wrote a macro that searched forward by going to the end of the buffer and reversing the search direction. Different syntax, same workaround.
Programming has not converged. We have cycled. SAIL → C → C++ → Java → Python → Go → Rust → TypeScript. Each one is an attempt to fix the problems of its predecessor. Each one inherited the problems anyway, wrapped in new syntax. You cannot escape memory management in any language; you can only choose how explicitly you think about it. The problem exists in every layer. You cannot escape the need to define data structures; you can only choose whether you write BEGIN DYNAMIC ARRAY [LENGTH] OF RECORD END in Pascal, typedef struct { int* bars; int bar_count; } Foo in C, class Foo { List<Bar> bars; } in Java, or type Foo = { bars: Bar[] } in TypeScript. The words are different. The problem is identical.
The truly depressing part is that we know this. The programming language research community understands that there are only so many fundamental problems, and most “new” languages are exploring the same solution space with different knobs turned. There are academic surveys of type systems that catalog every possible approach to checking types at compile time or runtime. There are papers on memory management strategies—manual, reference counting, mark-and-sweep, generational collection, concurrent collection. The research has been done. The problem space has been mapped. Yet every five years, someone writes a new language with a blog post titled “Here’s Why X Language Solves All the Problems Y Language Has” and the cycle continues. It is not dishonest, exactly. It is just incomplete. What they mean is “here’s a better tradeoff given our current constraints.” But that doesn’t sound as interesting as “we fixed programming.” So we don’t say it that way.
Programming is fundamentally a discipline of acceptable compromise. We compromise between performance and safety. Between developer velocity and correctness. Between the code we want to write and the code the hardware can actually execute. Between the language that would be perfect and the ecosystem that actually exists. Between measuring what you want to measure and measuring what your tools can measure. Between the memory model your language exposes and the memory model your CPU actually implements.
The evidence is not in the victories. The evidence is in the repetition. Fifty years of timers, and we still cannot measure code accurately. Fifty years of memory safety discussions, and we are still debating the same categories of failure. Fifty years of language design, and every problem a new language solves, it inherits two more. Every optimization brings performance up by 30% and adds a new class of subtle bugs. Every safety feature prevents one category of crash and makes it harder to express algorithms that the compiler cannot prove are safe.
This is not cause for despair. Despair is a luxury. The work continues because the work has to continue. Airplanes fly because engineers accepted that perfectly safe planes do not exist, and built them anyway. They accept certain failure modes—engine failure is survivable because planes have multiple engines. They design around the failures they accept. Databases persist data because programmers accepted that perfect consistency is impossible at scale, and built systems that fail gracefully. They accept that consistency might be eventual rather than immediate. They accept that a node might go down and data might be temporarily unavailable. They design around these failures. Programming persists because programmers accepted that perfect code is a myth, and ship imperfect code that mostly works most of the time.
The concrete action step, if there is one, is this: Stop pretending you are solving problems. You are making tradeoffs. When you write in Rust instead of C, you are not removing memory bugs; you are shifting when those bugs manifest and choosing to fail at compile time rather than runtime. Some bugs that would crash a C program at runtime will fail to compile in Rust. Other bugs that are possible in C are prevented by Rust’s borrow checker. You are shifting the distribution of failures, not eliminating them. When you use TypeScript instead of JavaScript, you are not preventing type errors; you are catching some of them earlier and accepting that others will still reach production. TypeScript will catch the error where you pass a string to a function expecting a number. It will not catch the error where you thought an object would have a property and it doesn’t. When you add a polyfill, you are not fixing a bug; you are acknowledging that different execution environments have different bugs and you are papering over the difference.
The discipline of programming will advance when we stop marketing tradeoffs as solutions and start understanding them as choices. Rust is not “better than C.” It is a different bet on which failures you will tolerate and when you are willing to discover them. Go is not “simpler than Java.” It is a different point in the complexity/performance/safety space, with different cost/benefit tradeoffs for different problem domains. TypeScript is not “the future of JavaScript.” It is a stopgap until JavaScript evolves or we give up and rewrite everything in something else. Kubernetes is not a “solution” to deployment complexity; it is an abstraction that trades deployment simplicity for operational complexity. You are no longer managing individual servers, but now you are managing a cluster orchestrator that has its own failure modes.
Programming is accounting. Keep your ledgers honest, and maybe the books will balance. Most of the time. Until they don’t, and you find out something was being recorded in the wrong column the entire time. That is the whole discipline. It has never gotten better. It has only gotten faster at failing in new ways.
