Plate 07
heapq.merge vs sorted(chain): Localhost Lab
Hands-on heapq.merge vs sorted(chain) multi-way merge: real records/s on pre-sorted lists, measured on Linux localhost today in this hands-on lab for SREs.
Aditya Challa4 min read
Intro — what this post promises
Merge k pre-sorted lists via heapq.merge vs sorted(itertools.chain(...)) into a full list (plus consume-only arms). This lab reports records/s on Linux localhost.
It is not sorted-vs-heapq-bisect push/pop (lab 46) and not nlargest (lab 66). This is multi-way merge of sorted inputs.
Related links:
- sorted vs heapq vs bisect localhost lab
- nlargest vs sorted slice localhost lab
- itertools chain vs flatten localhost lab
- zlib vs gzip compress localhost lab
- difflib vs set ops localhost lab
- configparser vs json localhost lab
- html escape vs manual localhost lab
- groupby vs manual localhost lab
Lab honesty (1 Oct 2026 IST): Python 3.13.5. Affiliates: 0. No Docker. Inputs already sorted lists in RAM.
Verdict up front (k=4 × 25 000 = 100 k records): sorted(chain) ~85.15 Mrec/s; heapq.merge→list ~9.82; consume-only merge ~8.54. When everything fits in memory and you need a list, sorted(chain) wins here; use merge for lazy streaming / low peak memory.
Arms
| Arm | Pattern |
|---|---|
list(heapq.merge(*runs)) | multi-way merge → list |
sorted(chain.from_iterable) | flatten then timsort |
sorted(chain(*runs)) | same, star form |
| merge / sorted consume | iterate results |
| chain consume only | no sort (baseline) |
Lab topology
Script: lab-evidence/108-heapq-merge-vs-sorted/results/run_lab.py.
Lead table — k=4 × 25 k (p50 Mrec/s)
| Arm | Mrec/s |
|---|---|
| sorted(chain.from_iterable) | 85.15 |
| sorted(chain(*runs)) | 82.85 |
| chain consume (unsorted) | 54.38 |
| sorted consume | 36.9 |
| heapq.merge → list | 9.82 |
| heapq.merge consume | 8.54 |
Timsort on already-structured int runs is extremely fast; merge pays heap maintenance per output.
Scale sketch (merge→list vs sorted(chain))
| Config | heapq.merge | sorted(chain) |
|---|---|---|
| k4 × 25k | 9.82 | 85.15 |
| k8 × 25k | 7.83 | 66.99 |
| k16 × 10k | 6.69 | 54.62 |
| k4 × 100k | 9.83 | 76.79 |
When merge still wins the design
- Inputs are lazy iterators you must not fully materialize.
- You only need the first N merged records (
islice(merge(...), N)). - Peak RAM matters more than CPU on huge runs.
If runs are already concrete lists and you need the whole output list, sorted(chain) was faster on this box.
Memory shape
sorted(chain) builds one big list (plus sort scratch). heapq.merge keeps k heads and yields — peak memory tracks the consumers, not a second full copy of every run. On this lab both inputs and outputs fit comfortably; production scale flips the choice.
Reading it
- Full in-memory materialize → prefer
sorted(chain)here. - Streaming / early-stop → prefer
heapq.merge. - Do not confuse with heapq for top-k (lab 66) or bisect insert (lab 46).
- Chain-without-sort is not a correct merge — baseline only.
Early-stop pattern
list(islice(heapq.merge(*runs), 1000)) keeps merge’s O(k) head state and stops after 1000 yields. sorted(chain(...))[:1000] still sorts everything first. If dashboards only need the leading merged page, that early-stop gap dominates any per-record microbench on full materialization.
Integer-run caveat
Our fixtures partition a contiguous integer range — Timsort loves that structure. Random comparable objects or reverse-ish runs can narrow the sorted(chain) lead. Re-measure with your key types before freezing a service default.
Pitfalls
- Calling
mergeon unsorted inputs (silent wrong order). - Building
list(merge(...))then wondering why it is slower thansorted. - Forgetting early-stop with
islicewhen merge’s laziness is the point. - Comparing to nlargest / single-heap patterns.
Reproduce
Evidence: summary.json, summary.txt.
Limits
One Linux box. Integer runs that partition a range (very sort-friendly). Not external-memory k-way merge.
Takeaway
At k=4 × 25 k, sorted(chain) ~85.15 Mrec/s beat heapq.merge→list ~9.82 Mrec/s. Use merge for lazy multi-way streams; use sorted(chain) when all runs are already in RAM and you need a full list.
Lab evidence
What I found running this
Lab 1 Oct 2026 IST. Python 3.13.5. k4×25k: sorted(chain) 85.15 Mrec/s; heapq.merge→list 9.82. Not lab 46/66. Affiliates: 0. Evidence: lab-evidence/108-heapq-merge-vs-sorted/. Ran the benchmark on Linux localhost and checked the full-list and consume-only arms.
Related links
Plate 58
groupby vs Manual Group: Localhost Lab
Hands-on itertools.groupby vs manual dict-of-lists: real records/s grouping pre-sorted key runs, measured on Linux localhost today in this lab for SREs.
Observability & SRE · 30 Sept 2026
Plate 12
islice vs list Slice Windows: Localhost Lab
Hands-on itertools.islice vs list slice window lab: real ops/s taking ranges from sequences, measured on Linux localhost in this hands-on lab for SREs.
Observability & SRE · 1 Oct 2026
Plate 88
mmap Write vs pwrite Region: Localhost Lab
Hands-on mmap MAP_SHARED write+msync vs pwrite region update: real MB/s with durability labels, measured on Linux localhost in this hands-on lab for SREs.
Observability & SRE · 1 Oct 2026