Plate 51
sorted vs heapq vs bisect: Ordered Insert Lab
A hands-on Linux localhost benchmark comparing batch sorting, bisect.insort, and heapq as ordered data arrives.
Aditya Challa5 min read
Intro — what this post promises
Need an ordered collection while values keep arriving? Folklore says “just use bisect.insort” or “use a heap.” This lab times three real patterns on Linux localhost:
- Batch: append unsorted, then one
list.sort/sorted(). - Online sorted list:
bisect.insortinto a growing list. - Heap:
heapq.heappush(and optional push+pop-all to drain).
We report inserts/s (p50) for growing N, plus a stream case: prefill M, then insert k more.
Related links:
- dataclass vs slots vs dict localhost lab
- json vs orjson vs msgpack localhost lab
- python re vs str methods localhost lab
- tempfile NamedTemporaryFile localhost lab
- atomic rename vs overwrite localhost lab
- Why your average latency graph is lying (p50 / p95 / p99)
- fork COW RSS vs spawn localhost lab
- getaddrinfo DNS resolve localhost lab
Lab honesty (1 Oct 2026 IST): Python 3.13.5. Random ints via random.Random(42). Affiliates: 0. Stdlib only — not sortedcontainers / btree.
Verdict up front: at N=5000, append+sort ~7.55M/s and sorted() ~8.89M beat bisect.insort ~2.62M (~0.35×). heapq.push ~17.6M if you only need heap order. At N=20k, insort collapses to ~0.86M (~1168 ns/op). Streaming into a large list: extend+sort often beats repeated insort.
What each tool actually guarantees
| Pattern | Invariant | Cost shape |
|---|---|---|
append + list.sort | fully sorted after one sort | amortize Timsort over batch |
sorted(iterable) | new sorted list | same as sort; allocates |
bisect.insort | list always sorted | binary search + O(n) memmove |
heapq.heappush | heap property (not fully sorted) | O(log n) per push |
| push + pop-all | drained ascending | push + n pops |
bisect looks up the insertion point in O(log n) — then Python’s list insert still slides every trailing element. That memmove is why the curve falls off a cliff as N grows. heapq only bubbles along a heap path; the list is not sorted end-to-end unless you drain it.
Lab topology
Each arm rebuilds from scratch (or from a fresh prefill copy for stream). Repeats: 5 for N≤5k / small streams, 3 for N=20k and M=50k. Script: lab-evidence/46-sorted-vs-heapq-bisect/results/run_lab.py.
Lead table — N=5000 inserts (p50)
| Arm | inserts/s | ns/op | vs append+sort |
|---|---|---|---|
| heapq.push | 17 585 324 | 57 | 2.33× |
| sorted() | 8 886 093 | 113 | 1.18× |
| append+sort | 7 553 806 | 132 | 1.00× |
| heapq push+pop-all | 4 136 347 | 242 | 0.55× |
| bisect.insort | 2 617 727 | 382 | 0.35× |
If you need a fully sorted list after a known batch, sort once. If you need online min/max, prefer heapq — do not confuse heap order with a sorted array. The push+pop-all arm is the honest “heap that yields sorted output” baseline (~4.14M here).
Scale — how N hurts bisect
| N | append+sort | bisect.insort | heapq.push |
|---|---|---|---|
| 100 | 25.2M | 15.3M | 24.5M |
| 1 000 | 10.5M | 5.68M | 22.7M |
| 5 000 | 7.55M | 2.62M | 17.6M |
| 20 000 | 6.84M | 0.86M | 19.4M |
At N=20k, insort is ~1168 ns/op — binary search is cheap; shifting list elements dominates. Timsort on a random batch stays in the ~6–8M/s band. Heap push stays ~17–19M because work per insert grows only logarithmically.
Tiny N (100) makes everyone look “fast enough.” The decision that matters is the curve: does your workload stay at hundreds of inserts, or grow into tens of thousands online?
Stream case — insert k=1000 into a large list
Prefill M, then insert 1000 random values (measure only the insert phase).
| Prefill M | bisect.insort | heapq.push | extend+sort |
|---|---|---|---|
| 1 000 | 2.98M | 14.2M | 11.8M |
| 10 000 | 0.81M | 4.26M | 5.96M |
| 50 000 | 0.20M | 0.88M | 1.73M |
At M=50k, extend+sort (~1.73M inserts/s) beats heap (~0.88M) and crushes bisect (~0.20M) for this batch size. Teaching point: online sorted list ≠ cheapest online structure. Buffer and sort when you can; use a heap when you need extract-min without a full sort.
Pitfalls we hit (and you will)
- Comparing heap push to sorted list.
heappushdoes not leavelistfully sorted. Fair “sorted output” arm is push+pop-all. - Reporting sum-of-repeats RPS. ops/s here uses p50 batch wall ÷ N.
- Tiny N. At N=100 everyone looks fast; the story is the curve as N grows.
- Wrong structure for the API. Need random access by rank → sorted list / bisect. Need peek-min → heap. Need occasional full order → batch sort.
insortin a hot ingest loop. Profile first; often a ring buffer + periodic sort wins.
When to pick what
| Need | Prefer |
|---|---|
| Batch of values, then one ordered list | append + sort / sorted() |
| Continuous extract-min / priority queue | heapq |
| Must stay sorted for bisect lookups | bisect + rare inserts, or a tree lib |
| Many inserts into large sorted list | buffer → extend+sort, not insort in a loop |
Reproduce
Evidence: /workspace/lab-evidence/46-sorted-vs-heapq-bisect/results/.
Closing
bisect.insort is correct and slow at scale because every insert can move O(n) elements. list.sort amortizes; heapq stays O(log n) if heap order is enough. On this box at N=5k: push ~17.6M, sort batch ~7.6–8.9M, insort ~2.6M; at N=20k insort falls to ~0.86M. Stream into M=50k: extend+sort ~1.73M > heap ~0.88M > bisect ~0.20M. Measure your N — folklore “always insort” does not survive that curve.
Lab evidence
What I found running this
Lab 1 Oct 2026 IST. Python 3.13.5. N=5000: append+sort 7.55M/s; sorted() 8.89M; bisect.insort 2.62M; heapq.push 17.6M; push+pop-all 4.14M. N=20000 bisect 0.86M. Stream M=50k: extend+sort 1.73M > heap 0.88M > bisect 0.20M. Affiliates: 0. Evidence: lab-evidence/46-sorted-vs-heapq-bisect/.
Related links
Plate 17
platform vs os.uname Inventory: Localhost Lab
Hands-on platform.platform vs os.uname host inventory lab: real ops/s plus cache notes, measured on Linux localhost today in this hands-on lab for SREs.
1 Oct 2026
Plate 75
uuid.uuid4 vs uuid.uuid1: Localhost Lab
Hands-on uuid.uuid4 vs uuid.uuid1 ID generation lab: real ops/s plus version/node checks, measured on Linux localhost today in this hands-on lab for SREs.
1 Oct 2026
Plate 50
signal vs threading.Event Wakeup: Localhost Lab
Hands-on signal SIGUSR1 vs threading.Event wakeup lab: real p50 latency in microseconds, measured on Linux localhost today in this hands-on lab for SREs.
1 Oct 2026