The price the counter cannot see
The guarantee is not free. To see the price, measure where the BST was already doing fine — on shuffled input.
$ algo rotate -in s10k.jsonl
n = 10000, inserted in the order given
build cmps build time rotations height
BST (11) 157025 3.33ms - 30
AVL (12) 120660 4.551ms 6922 16
rotations per insert: 0.692
worst |balance| anywhere in the AVL tree: 1
lookups: BST 16.703 cmps, AVL 12.540 cmps
The counter says the AVL is cheaper
BST 157,025 comparisons
AVL 120,660 comparisons
23% fewer. And that is true: the tree is shallower, so every insertion walks a shorter path, and the path length is the comparison count.
By the only instrument this course has used since lesson 1, balancing costs less than nothing.
The clock disagrees
The build time column in that output is one sample, and it is inside the
noise — at 10,000 records both versions land between 2 and 3 ms and change
places between runs. Lesson 6's rule applies: when the difference is small, one
measurement means nothing.
So measure it properly:
$ go test -run XXX -bench 'Build|Lookup' -benchtime=20x -count=5
BenchmarkBuildBST-32 20 1859315 ns/op
BenchmarkBuildBST-32 20 1870065 ns/op
BenchmarkBuildBST-32 20 1800945 ns/op
BenchmarkBuildBST-32 20 1922465 ns/op
BenchmarkBuildBST-32 20 1940315 ns/op
BenchmarkBuildAVL-32 20 3202495 ns/op
BenchmarkBuildAVL-32 20 3352500 ns/op
BenchmarkBuildAVL-32 20 2894720 ns/op
BenchmarkBuildAVL-32 20 3288470 ns/op
BenchmarkBuildAVL-32 20 2817325 ns/op
Five runs each, and the ranges do not overlap: BST 1.80–1.94 ms, AVL 2.82–3.35 ms.
Building the AVL is about 1.65× slower — while doing 23% fewer comparisons.
Where the difference went
Into a column the counter cannot see:
rotations 6922
A rotation is four pointer assignments and two height recomputations. It is not a
key comparison, so c.Hit() does not fire for it — and rightly does not: the
counter measures comparisons, not all work.
Lessons 4, 6 and 11 each showed the counter running out of reach. This is the fourth, and the sharpest, because the two readings point in OPPOSITE directions:
The instrument said the AVL was cheaper. The clock said it was dearer. Both are right — they are measuring different things.
The counter is still the correct tool for an algorithm's complexity: it does not tie you to a machine and it does not change between runs. But it cannot see the constant, and the entire cost of balancing lives in the constant.
The other half: lookup
BenchmarkLookupBST-32 20 1514950 ns/op (1.45–1.56 ms)
BenchmarkLookupAVL-32 20 1246885 ns/op (1.25–1.33 ms)
Lookup in the AVL tree is about 1.16× faster, because the tree is shallower (16 against 30).
The trade, precisely
| BST (11) | AVL (12) | |
|---|---|---|
| insert, shuffled input | 1.80–1.94 ms | 2.82–3.35 ms (1.65× slower) |
| lookup, shuffled input | 1.45–1.56 ms | 1.25–1.33 ms (1.16× faster) |
| height, shuffled input | 30 | 16 |
height, -sorted |
10,000 | 14 |
| memory per node | 88 B | 96 B (the h field) |
You pay on every insertion to avoid a case that may never arrive.
And that pays off if at least one of three things is true:
- you cannot guarantee the insertion order — and usually you cannot, because it comes from a database, a file or a user;
- you read more often than you write — then a 1.16× faster lookup sits on the scales against a 1.65× slower insert, and the ratio decides which way they tip;
- the worst case is unacceptable rather than merely unpleasant.
If none of them is true — if you generate the data yourself, shuffled, once, and only read it afterwards — lesson 11's tree is enough. Or, as that same lesson showed, a sorted slice is.