The rule that reconciles all three
Three measurements, three different answers, one rule.
| lesson | standard library | result |
|---|---|---|
| 3 | container/list |
a tie — 50 hops against 50 |
| 6 | sort.Search |
won — 15 ns against 18 |
| 13 | container/heap |
lost — by 1.66× |
The temptation is to draw a conclusion about the standard library. But three different answers mean the question was never about it.
The rule
An abstraction is free when the compiler can see through it. It is expensive when it forces THE DATA TO BE BOXED on an operation too small to hide the cost.
(Drill 4 separates boxing from table dispatch and shows that boxing is what decides it here. Dispatch costs something too — just not a measurable amount in this case.)
Two conditions, and the second matters as much as the first. A call through a table costs about the same whatever it wraps — one comparison or an entire file read. What changes is not the cost but the ratio.
How the three fit
Lesson 6 — the compiler sees everything. sort.Search is handed a closure
created right there. The concrete type is known, so Go inlines it; after
optimisation what remains is the same loop you wrote by hand. Zero boxing, zero
calls. The abstraction disappears.
Lesson 13 — the compiler sees nothing. container/heap demands an interface.
Elements travel through any (boxing, 20,000 allocations), and Less and Swap
go through a table. And the operation being wrapped is one comparison — the
smallest there is. The ratio is as bad as it gets.
Lesson 3 — there was nothing to see through. container/list's methods are
defined on concrete types (*List, *Element), not on an interface. No
table, no dynamic dispatch. The only any in it is the stored Value, boxed once
when an element is inserted — and lesson 3 was measuring the walk, not the
insert. Hence the tie: nothing was in the way.
What follows in practice
Not "use the standard library" and not "hand-roll hot code". Neither survives three measurements in a row.
Ask three questions:
- Is the type known at the call site? A closure, a concrete type, a generic parameter — yes. An interface — no.
- Does the data cross an
anyboundary? If so, anything bigger than a pointer is copied to the heap. AnItemis 56 bytes; an interface value is 16. - How big is the operation being wrapped? One comparison — the cost shows. A file read — you will never notice it.
"Yes, no, small" is sort.Search. "No, yes, small" is container/heap. Which is
exactly how they came out.
And when any of it matters
In lesson 6 sort.Search was faster and shorter and already tested. That
choice is easy.
Here the hand-written heap wins by 1.66× — but that is 0.8 ms over 10,000
elements. Lesson 6 already showed what differences of that size look like beside
real work: transit's trie argument fits entirely inside a tenth of a millisecond
and decides nothing because of it.
1.66× matters when the operation runs millions of times. Your algo top does
not. Dijkstra on a 10,000-node graph does, and step 8 shows what it costs in
practice.
So the answer is not "hand-roll your heaps". The answer is: know why one abstraction is free and another is not, and be able to measure which case you are in.