What the Numbers Say
Real nanosecond traces from 11 languages, captured in Docker on identical hardware. Every number on this page comes from instrumented code you can run yourself: github.com/adamzwasserman/honest-code-traces
Correction, 3 and 4 October 2026. The first version of these traces measured the wrong thing. It timed the dishonest path in six pieces and the honest path in four, with two clock reads around each piece, so the dishonest side paid for more clock reads than the honest side before either ran a line of its own code. Three more defects affected the labels and the singleton row. We rewrote all eleven harnesses to time the whole scenario, so both sides pay the cost of the clock once per pass, and on 4 October we rebuilt the dishonest class so its calls must run in order. Every number on this page comes from the rewritten harnesses, and all eleven numbers changed. The corrected ratios came out higher than the old ones. That is not why we changed the method: the old numbers did not measure what we said they did. The old measurements stay in the repository for anyone who wants to compare.
1. The Ranking
We timed one whole order in each language: three items and a coupon, with tax. We wrote it twice. The dishonest version is a class with separate methods, addItem, applyCoupon, calcTotal, calcDiscount and calcTax. They must run in that order, because each reads fields the others wrote, and the class looks up the coupon and tax tables through singletons. The honest version is two pure functions over plain data: one applies the coupon and one adds the tax. Both return the same total, 87.45 for the sample order. The dishonest class also stamps itself with the time after every item and coupon. We measured the stamps apart and left them out of the dishonest figure, so the dishonest figure pays for no clock reads. Each figure is a median on one rented eight-core server, inside one Docker image.
We checked every language with a benchmarking tool: Google Benchmark for C++, Go’s own testing package, mitata for TypeScript, JMH for Java and Kotlin, BenchmarkDotNet for C#, the Benchmark package for Swift, benchmark_harness for Dart, pyperf for Python, benchmark-ips for Ruby and PHPBench for PHP. We also ran our own hand-written loop in every language, which times 1,000 passes between two clock reads. Where the tool and the loop disagreed, we kept the pair with the smaller ratio. That was the loop for TypeScript and PHP, and a tool for the rest.
| Language | Dishonest | Honest | Ratio |
|---|---|---|---|
| Go | 311 ns | 42.8 ns | 7.27x |
| C++ | 104 ns | 19.5 ns | 5.35x |
| Swift | 282 ns | 114 ns | 2.47x |
| Python | 1,519 ns | 693 ns | 2.19x |
| Ruby | 2,248 ns | 1,137 ns | 1.98x |
| C# | 88.2 ns | 49.8 ns | 1.77x |
| PHP | 964 ns | 600 ns | 1.61x |
| Kotlin | 42 ns | 30.5 ns | 1.38x |
| TypeScript | 93 ns | 69.5 ns | 1.34x |
| Dart (precompiled) | 140 ns | 120 ns | 1.16x |
| Java | 38.1 ns | 36.2 ns | 1.05x |
| Dart | 75.4 ns | 72.2 ns | 1.04x |
The chart is ordered by honest speed, fastest first, and the table by ratio, largest first. The ratio column shows how many times slower the dishonest version is. Honest code was never slower than dishonest code in any language we tested. It was faster by a real margin in 10 of 12, from 1.2x (Dart (precompiled)) to 7.3x (Go). In Java and Dart the two are the same within measurement error.
2. The Surprise
Does honest code in a "slower" language beat dishonest code in a "faster" one? Here it does.
C++ compiles ahead of time to native code. TypeScript runs on V8, which compiles while the program runs. Honest TypeScript takes at most 99 ns, and dishonest C++ takes at least 102 ns. That is the same speed within measurement error: a language that compiles while it runs keeps pace with C++ compiled ahead of time, when the C++ is written the typical way. Our own hand-written loop puts honest TypeScript 1.5 times ahead, and we report the tool figure, which is the smaller gap. Honest TypeScript also beats dishonest Go and Swift, by 2.8 times or more. It does not beat dishonest C#, Java and Kotlin. Dishonest Java (38.1 ns) and Kotlin (42 ns) also beat dishonest C++ (104 ns).
Every language we tested, whether compiled ahead of time, compiled at run time or run by a bytecode interpreter, shows honest code at least as fast as dishonest code. Where the gap is large, as in Go and C++, the cost comes from the shape of the code: mutable fields, calls that must come in the right order, and global lookups.
3. Why the Gap Exists
The dishonest path in the demo runs 15 steps: 5 method calls, 4 field writes, 1 computation, 2 singleton lookups, 1 table lookup and 2 timestamp writes. The honest path runs 9: 2 function calls, 2 argument passes, 3 pure computations and 2 return values. Same business logic, same result. The dishonest class needs its methods called in the right order. The honest functions do not, because each takes what it needs as arguments.
We wrote the dishonest code with care to represent the skill and training of the average programmer, not those of the best programmer. For honest code that distinction matters little, because the honest shape leaves much less room for variation. Two pure functions over plain data look much the same no matter who writes them. The one skill that does set programmers apart in Honest Code is decomposition: splitting a complex code path into functions that each do one thing. That skill needs a code path complex enough to split, and an example short enough to fit on a page does not have one.
The demo on the home page is a reconstruction of that structure. It scales the steps so each side adds up to the measured total, and the proportions between steps follow an earlier per-step timing, so read the totals as the measurement and the steps as the shape.
4. Beyond Method Dispatch
The numbers above measure one anti-pattern: mutable classes with singleton lookups. Dishonest code takes many other forms. Most of them are better described by counting than by timing, so here we count. Each row comes from the demo in the chapter named, and each count is a fact about the code in that demo, not a measurement of how long it runs. The book goes into detail about why each of these is an anti-pattern, in the chapter named in the second column.
| Anti-pattern | Chapter | What we count | Dishonest | Honest | Ratio |
|---|---|---|---|---|---|
| Try/catch recovery chain | 7 | address spaces touched | 6 | 2 | 3.0x |
| Mock-heavy test | 9 | lines of test code | 46 | 11 | 4.2x |
| Scattered state | 5 | places the same value lives | 6 | 1 | 6.0x |
| Middleware chain | 1 | stack frames | 13 | 4 | 3.2x |
| Object graph | 3 | heap allocations | 6 | 3 | 2.0x |
| ORM + cache miss path | 8 | steps to answer one request | 10 | 3 | 3.3x |
The pattern holds across every anti-pattern
Method dispatch overhead (Section 1) is not a special case. The same structural tax appears when you scatter state across locations, when you build deep middleware chains, when you chase pointers through object graphs, when you wrap exceptions in recovery ceremony, and when you bury your test logic under mock scaffolding. In every case, the honest alternative has fewer steps, fewer address spaces and fewer indirections. The counts differ by 2.0x to 6.0x, and the direction never changes.
5. The Point
Performance is a happy side effect. It is not why honest code matters.
Honest code is objectively more testable. Where its inputs come from bounded sets, such as a region or a coupon code, the cases are finite, and you can test every one of them and know that you did. Numbers are not bounded, so they do not form a finite set, but limit testing handles them productively. A pure function’s result depends on its inputs alone, so testing the edges and the values around them covers the ground that matters. A class with mutable fields has no such handle. Its result depends on the history of calls as well, calls repeat without bound, the number of possible call sequences is infinite, and nobody can test them all. Honest code is also objectively easier to reason about. You never have to hold every state the whole application can be in, because no function changes anything outside itself. You read the code one function at a time. Reasoning becomes a question of how to compose the functions into chains, and that is an easier task than trying to anticipate every possible state change.
You can count it. The class we timed has seven calls: three addItem, applyCoupon, calcTotal, calcDiscount and calcTax. Run them in every one of the 5,040 possible orders, and the class returns 36 different totals. Only 30 of those orders return the right one. The honest version has two functions, and only one order exists, because the second needs the result of the first. The script that prints these counts is harness/demos/order_dependence.py in the traces repository.
The dishonest path has 6 state mutations and 2 global singleton lookups per order. Each mutation is a place where state can be corrupted. Each singleton is a place where two threads can disagree about reality. The rescue path has zero mutations and zero globals. The same values arrive at the same answer through pure computation. What is noteworthy is that this does not require learning a functional programming language or an exotic type system. All it asks is that you stop using objects for everything, as we have all done for so long.
When your Order class has 6 mutable fields that 5 different methods write, your test suite needs to cover the interaction of every possible call sequence. When your apply_coupon() function takes the items and a coupon table and returns a total, a discount and a subtotal, you test it with one assertion. assert apply_coupon(items, "SAVE10", coupons) == (100.0, 10.0, 90.0), for items that total 100. Done.
The dishonest test has to set up three mock objects, five stubbed methods and a dependency-injection container before it can run one assertion. The three mock objects stand for three hidden dependencies your code cannot function without. The 2 singleton lookups in your dishonest code are 2 invisible couplings to global state. The 6 state mutations are 6 moments where your object can be observed in an inconsistent state by another thread, another method, another test. No amount of JIT compilation fixes that.
In every language, runtime and VM architecture we tested, honest code was never slower, and in most of them it was faster by a real margin. The data speaks for itself. Good architecture is what works best.
Source code and raw data: github.com/adamzwasserman/honest-code-traces
Docker image with all 11 runtimes. One command to reproduce every number on this page.
Why compiled versus interpreted is the wrong question: isitcompiled.com shows how optimizer settings change these times in Java, Python, JavaScript and C++.