Katabench
Start free
← All posts

The premature optimization myth: what Knuth actually said

There's a quote that gets deployed in code review the way a goalkeeper clears a ball: hard, fast, and as far away as possible. Someone points out that your method allocates a list inside a hot loop, and back comes the reflex: "premature optimization is the root of all evil." Thread closed. Nobody has to think about performance, which was the real goal all along.

It's one of the most quoted lines in our field, and one of the least read. Because if you actually go back to Donald Knuth's 1974 paper Structured Programming with go to Statements, the sentence says something close to the opposite of how it's used.

What Knuth actually wrote

Here is the full passage, not the bumper-sticker version:

Programmers waste enormous amounts of time thinking about, or worrying about, the speed of noncritical parts of their programs, and these attempts at efficiency actually have a strong negative impact when debugging and maintenance are considered. We should forget about small efficiencies, say about 97% of the time: premature optimization is the root of all evil. Yet we should not pass up our opportunities in that critical 3%.

Read it twice. The famous clause is sandwiched between two qualifiers that completely change its meaning. Knuth isn't saying don't think about performance. He's saying don't fiddle with the noncritical 97%, so that you have the attention and judgment left to nail the critical 3%. His very next sentence is even more telling: a good programmer "will be wise to look carefully at the critical code; but only after that code has been identified."

The quote is a call to prioritize optimization, not to avoid it. It assumes you can already tell the 3% from the 97%. That's the part everyone skips.

The program, by share of code

the critical ~3%

noncritical, leave it alone · ~97% of the code

The same program, profiled

where the time actually goes

that same sliver of code, now most of the elapsed time

optimization effort spent in the noncritical 97%: nothing anyone can measure

Knuth's advice has two halves: leave the noncritical code alone, and do not pass up the critical 3%. Both halves assume you can tell one from the other.

The skill the quote presupposes

So how do you know where the 3% lives? The lazy answer is "profile it." And yes, you profile before you do invasive surgery on a real system. But profiling tells you where the time went after the fact. It does not write the code in the first place. By the time the profiler lights up, the shape is already chosen, the data structure is already wrong, and the allocation is already happening a million times.

The engineers who consistently land in the good 3% aren't profiling more than everyone else. They've internalized a cost model. They feel the difference between a Dictionary lookup and a List.Contains the way you feel the difference between a screwdriver and a hammer. They reach for the right shape first, not because they "optimized," but because the right shape is simply what their hands do.

That instinct is invisible, which is exactly why it gets dismissed as premature optimization. It doesn't look like optimization at all. It looks like writing normal code.

When idiomatic is fast

Here's the part that breaks the false dichotomy. Most of the time, the fast version isn't some hand-rolled, unreadable monster you bolt on later. It's the clean, idiomatic version a senior writes by default.

Take a common task: you have a list of orders and a set of flagged customer IDs, and you want the orders belonging to flagged customers. The natural-feeling first draft:

public List<Order> FlaggedOrders(List<Order> orders, List<int> flaggedCustomerIds)
{
    var result = new List<Order>();
    foreach (var order in orders)
    {
        if (flaggedCustomerIds.Contains(order.CustomerId))
        {
            result.Add(order);
        }
    }
    return result;
}

This passes every correctness test you'll ever write for it. It reads fine. And it's O(n × m), because List.Contains is a linear scan every single time. With 200,000 orders and 5,000 flagged IDs, that's a billion comparisons.

Now the version a senior writes without thinking twice:

public List<Order> FlaggedOrders(List<Order> orders, List<int> flaggedCustomerIds)
{
    var flagged = flaggedCustomerIds.ToHashSet();
    return orders.Where(o => flagged.Contains(o.CustomerId)).ToList();
}

It's shorter. It's more readable. It says what it means: "give me the orders whose customer is flagged." And it's O(n), because HashSet.Contains is constant time. On that same input the felt contrast is brutal: roughly 1,400 ms collapsing to about 6 ms, with a fraction of the allocations.

Nobody optimized anything here. The senior didn't profile, didn't micro-tune, didn't sacrifice clarity. They just knew, in their bones, that membership testing wants a set. The idiomatic choice was the fast choice. This is what Knuth meant by not passing up the critical 3%, except the cost was zero, because the instinct paid for it up front.

Approach Shape 200k orders Reads as
List.Contains in a loop O(n × m) ~1,400 ms "loop and check"
HashSet + LINQ O(n) ~6 ms "filter by membership"

You can't profile your way to instinct

The uncomfortable truth is that the cost model isn't something you read about; it's something you feel, repeatedly, until it's automatic. You have to be burned by the List.Contains enough times, and rewarded by the HashSet enough times, that the right shape becomes the first thing your hands reach for. That's not "knowing Big-O." Plenty of people can recite Big-O and still write the billion-comparison loop on a Tuesday afternoon.

This is the whole reason we built Katabench around a feedback loop that fails you on performance, not just correctness. Every algorithm puzzle runs against hidden tests with hundreds of thousands of elements and a per-test time budget. The List.Contains version doesn't get a polite note; it times out, with the overage shown, and you feel the 1,400 ms. The HashSet version comes back in single-digit milliseconds. Do that across a few dozen puzzles and the cost model stops being trivia and starts being reflex. (If you want the mechanics of how that grading runs, the how-it-works page walks through the compile-and-sandbox pipeline.)

So the next time someone reaches for the Knuth quote to avoid thinking, you can hand them the whole sentence. The 3% is real, and so is the 97%. The skill is telling them apart on sight, and the only way to get there is reps. That's what the algorithm catalog is for: enough felt contrasts that the right shape becomes the one you write first.

Get new puzzles and .NET tips in your inbox

A short note when fresh kata land, plus the C# and performance tricks behind the grading. No spam, unsubscribe anytime.