Programming Languages

Rob Pike's Programming Rules Are Still Right

In 1989, Rob Pike — who would later co-create Go, UTF-8, and Plan 9 — wrote down five rules of programming. They're short enough to fit on an index card and profound enough that the programming world has been arguing about them for 37 years. Most developers have seen at least some of them quoted in blog posts or conference talks. Fewer have actually internalized them, which is a shame, because they'd save a lot of wasted effort.

The rules are deceptively simple. They're not about syntax or architecture patterns. They're about where programmers consistently waste time and how to stop doing it.

The Five Rules

Let me state them plainly before dissecting them:

  1. You can't tell where a program is going to spend its time. Bottlenecks occur in surprising places, so don't try to second guess and put in a speed hack until you've proven that's where the bottleneck is.
  2. Measure. Don't tune for speed until you've measured, and even then don't unless one part of the code overwhelms the rest.
  3. Fancy algorithms are slow when n is small, and n is usually small. Fancy algorithms have big constants. Until you know that n is frequently going to be big, don't get fancy.
  4. Fancy algorithms are buggier than simple ones, and they're much harder to implement. Use simple algorithms as well as simple data structures.
  5. Data dominates. If you've chosen the right data structures and organized things well, the algorithms will almost always be self-evident. Data structures, not algorithms, are central to programming.

Rules 1 and 2 are about optimization. Rules 3 and 4 are about complexity. Rule 5 is about design. Together, they form a philosophy that's fundamentally about humility — admitting that our intuitions about performance are wrong, that complexity has costs we underestimate, and that good data structures matter more than clever code.

Rule 1: You Don't Know Where the Bottleneck Is

This is the rule developers violate most confidently. 'I know this function is slow because it has a nested loop.' 'I should use a hash map here because lookups are O(1).' 'I'll preallocate this array because allocation is expensive.' These sound reasonable. They're often wrong.

I've profiled enough production systems to have a collection of examples where the intuitive bottleneck wasn't the actual bottleneck. A system where everyone assumed the database was the bottleneck, but profiling showed that JSON serialization was consuming 60% of request time. A data pipeline where the 'expensive' matrix multiplication took 5% of runtime while CSV parsing took 70%. A web application where the team optimized their database queries for months while the real bottleneck was DNS resolution on every outbound HTTP request.

The human brain is a terrible profiler. We overweight operations that seem conceptually expensive (database queries, network calls) and underweight operations that seem cheap (string concatenation, JSON parsing, memory allocation). Modern hardware makes this worse — CPU caches, branch prediction, and out-of-order execution mean that the relationship between code complexity and execution time is deeply unintuitive.

Rule 2: Measure First, Then Optimize

This is Rule 1's practical corollary. Don't optimize based on intuition. Profile. Find the actual hotspot. Then optimize that, and only that.

The second half of this rule — 'don't unless one part of the code overwhelms the rest' — is equally important and less often quoted. If your profiler shows that runtime is evenly distributed across 20 functions, each taking 5%, there's no single bottleneck to optimize. Making one function 2x faster saves 2.5% of total runtime. That's rarely worth the added complexity. You need to find a fundamentally different approach rather than optimizing individual functions.

# Profile first. Always.
# Python
python -m cProfile -s cumulative my_script.py
# Node.js
node --prof app.js
node --prof-process isolate-*.log > profile.txt
# Go
go test -bench=. -cpuprofile=cpu.prof
go tool pprof cpu.prof
# Rust
cargo flamegraph
# General (Linux)
perf record ./my_program && perf report
# The flamegraph (brendangregg.com/flamegraphs) is the most
# informative visualization. It shows you exactly where time
# is spent, in a format that makes bottlenecks visually obvious.

Rule 3: Fancy Algorithms Are Slow When N Is Small

This is the rule that computer science education gets backwards. We teach O(n log n) is better than O(n²), which is true asymptotically. But for n = 20, a well-implemented O(n²) insertion sort is faster than an O(n log n) merge sort because of constant factors, cache behavior, and overhead.

Real-world examples abound. Linear search through a sorted array of 50 elements is faster than binary search because linear search has perfect cache behavior and no branch mispredictions. A simple linked list beats a balanced binary tree for collections under ~100 elements because pointer chasing through a tree destroys cache locality. Hash maps have O(1) amortized lookup but the constant is high enough that linear search through an array is faster for collections under ~30-50 elements.

Standard libraries know this. Python's sorted() uses Timsort, which falls back to insertion sort for small subsequences. C++'s std::sort switches to insertion sort below a threshold (typically 16-32 elements). Rust's sort_unstable uses a combination of quicksort and insertion sort. The 'fancy' algorithm is only used where n is actually big enough for it to win.

The broader lesson: know your n. If you're choosing between a simple O(n²) algorithm and a complex O(n log n) one, ask yourself how big n will actually be in practice. If it's under a few hundred, the simple algorithm is almost certainly fine — and it'll be easier to write, debug, and maintain.

Rule 4: Simple Is Better Than Clever

Rule 4 extends Rule 3 beyond performance. Fancy algorithms aren't just slower for small n — they're buggier. A red-black tree has more edge cases than a sorted array. A lock-free concurrent data structure has more subtle failure modes than a mutex-protected one. A custom memory allocator has more ways to corrupt memory than the system allocator.

I've seen teams spend weeks implementing and debugging a custom LRU cache with O(1) operations when a simple bounded array with linear eviction would have been written in an afternoon, correct on the first try, and fast enough for their workload (which had at most a few hundred cache entries).

The cost of complexity isn't just in the initial implementation. It's in every future developer who has to understand, modify, and debug it. A simple algorithm that everyone on the team understands is worth more than a clever one that only the original author can maintain. And the original author, six months later, is effectively a different person who has also forgotten how it works.

Debugging is twice as hard as writing the code in the first place. Therefore, if you write the code as cleverly as possible, you are, by definition, not smart enough to debug it. — Brian Kernighan

Rule 5: Data Dominates

This is Pike's most important rule and the one most overlooked by developers who focus on algorithms and design patterns. The claim: if your data structures are right, the algorithms follow naturally. If your data structures are wrong, no amount of algorithmic cleverness will save you.

Fred Brooks said something similar: 'Show me your flowcharts and conceal your tables, and I shall continue to be mystified. Show me your tables, and I won't usually need your flowcharts.' Linus Torvalds echoed it: 'Bad programmers worry about the code. Good programmers worry about data structures and their relationships.'

This principle shows up constantly in practice. A codebase that stores user permissions as a flat list of permission strings will accumulate complex, error-prone checking logic scattered throughout the code. Restructure the data as a role hierarchy, and the checking logic becomes trivial. A system that stores events as JSON blobs will need complex parsing and validation at every consumer. Structure the events as typed records with explicit schemas, and the consumers simplify dramatically.

# Bad data structure → complex algorithm
class OrderSystem:
def __init__(self):
self.orders = []  # flat list of all orders
def get_user_orders(self, user_id):
return [o for o in self.orders if o.user_id == user_id]  # O(n)
def get_pending_orders(self):
return [o for o in self.orders if o.status == 'pending']  # O(n)
def get_user_pending_orders(self, user_id):
return [o for o in self.orders
if o.user_id == user_id and o.status == 'pending']  # O(n)
# Better data structure → algorithms become obvious
class OrderSystem:
def __init__(self):
self.orders_by_user = {}      # user_id → [orders]
self.orders_by_status = {}    # status → [orders]
def get_user_orders(self, user_id):
return self.orders_by_user.get(user_id, [])  # O(1)
def get_pending_orders(self):
return self.orders_by_status.get('pending', [])  # O(1)
# The right data structure makes the code self-evident.
# You don't need to think about algorithms at all.

How Go Embodies These Rules

It's hard to look at Pike's rules without seeing Go's design philosophy in embryo. Go, which Pike co-created 20 years after writing these rules, is a language that systematically favors simplicity over cleverness.

  • No generics (initially) — force simple data structures. (Generics were added in Go 1.18, but only after years of resisting them until a simple-enough design was found.)
  • No operator overloading — code means what it looks like.
  • No implicit type conversions — explicitness over cleverness.
  • No exceptions — handle errors where they occur.
  • Minimal standard library algorithms — use slices and maps, not fancy data structures.
  • Built-in profiling (pprof) — measure, don't guess.

Go is often criticized as 'boring' by developers who prefer more expressive languages. That's exactly the point. Pike's rules are a recipe for boring code — code that's simple, measurable, and built on good data structures rather than clever algorithms. Go is what happens when you turn that recipe into a language.

Where the Rules Don't Apply

No set of rules is universal, and Pike's have legitimate exceptions. Performance-critical systems (game engines, database internals, compilers) sometimes need fancy algorithms because their n genuinely is large. Infrastructure code that runs millions of times per second justifies optimization that application code doesn't. And sometimes the 'simple' algorithm has O(n³) complexity that's genuinely unacceptable even for modest n.

The rules are heuristics, not laws. Their value is in correcting the common bias: developers tend to optimize too early, use algorithms that are too complex, and think too much about code and too little about data structures. Pike's rules push against those tendencies. If you find yourself in the rare situation where the opposite bias applies — where you genuinely need more complexity, not less — then by all means, get fancy. But measure first.

Thirty-seven years after Pike wrote them down, the rules remain some of the best programming advice ever published. Not because they're surprising — most experienced developers, reading them, think 'yes, obviously.' The value is in having them stated clearly enough to apply consistently. The next time you reach for a red-black tree, a custom allocator, or an 'optimization' you haven't profiled — remember: measure first, keep it simple, and get the data structures right.