Adam Sawicki’s essay — written for Programista in 2013 and back on Hacker News this week — opens with the claim that picking C++ buys you nothing by itself. Performance comes from knowing where your data sits in memory, and from habits you form while writing ordinary code, not from an optimization phase that happens after the program works. He calls the approach Data-Oriented Design.
What that means in practice:
- Design around the data first, algorithms second. It does not forbid classes — it forbids letting abstraction hide layout.
- Small objects scattered across the heap with pointers between them cost you twice: cache misses while traversing, and the inability to parallelize, because a virtual method tells you nothing about what state it touches. Mutexes then serialize what looked parallel.
- The memory hierarchy, on a 3 GHz machine where a simple op is 0.33 ns: L1 is 3 cycles, L2 is 14, RAM is 250, disk is 45 million, the network is 240 million.
- Caches move whole 64-byte lines, not bytes. Keep values used together adjacent (
std::vector, plain arrays), know your type sizes and padding, and pack into small types — the unpacking instructions cost less than the miss they avoid. - Asymptotics can lose to constants.
std::setandstd::mapallocate every element separately and link them with pointers, so an O(log n) lookup can be slower in practice than building an array once, sorting it, and binary searching. - The “performance pyramid,” each level an order of magnitude slower than the one above: arithmetic → transcendentals and division → RAM access → dynamic allocation → system calls and handles → external I/O. Compute a reciprocal once instead of dividing in a loop; batch I/O instead of per-byte calls; do disk and network work off-thread.
- AOS versus SOA: for a particle system, separating hot data (positions, velocities) from cold data (colors) is a layout decision, not an algorithmic one.
- “You can’t beat the library authors” is false. General-purpose code knows nothing about your constraints. If you only allocate fixed-size objects with a known upper bound, a preallocated free-list allocator beats
mallocoutright. - The compiler is more conservative than you think. In
AddVectorToArray(vec3* tab, size_t n, const vec3* s), it cannot provesdoesn’t point intotab, so it re-reads*severy iteration. Copy to a local, pass by value, or use__restrict. - One measured example: constructing a
std::stringinside a 2-million-iteration loop took 0.36 s. Declaring it outside and callingclear()each pass took 0.27 s — 25% for two lines, because the string keeps its capacity andclear()frees nothing. - The Knuth quote, in full: “We should forget about small efficiencies, say about 97% of the time. Premature optimization is the root of all evil.” It referred to a 12% loop speedup using
goto. It was never advice to ignore performance.
The profiler-last approach fails in two directions. Sometimes no single function dominates — everything is a little slow because the program is generally poorly written — and sometimes the hot function can only be fixed by redesigning the data structures and rewriting large parts of the program. Either way, the decision was made long before the profiler ran.
Nothing here requires assembly or heroics. Every recommendation is a design choice or a coding habit, which is the real argument against reading Knuth as permission to stop caring about performance.