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.
4 min read
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:
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.
| 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) |
Script: lab-evidence/108-heapq-merge-vs-sorted/results/run_lab.py.
| 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.
| 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 |
islice(merge(...), N)).If runs are already concrete lists and you need the whole output list, sorted(chain) was faster on this box.
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.
sorted(chain) here.heapq.merge.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.
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.
merge on unsorted inputs (silent wrong order).list(merge(...)) then wondering why it is slower than sorted.islice when merge’s laziness is the point.Evidence: summary.json, summary.txt.
One Linux box. Integer runs that partition a range (very sort-friendly). Not external-memory k-way merge.
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
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.
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.
Lab1 Oct 20264 min read
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.
Lab1 Oct 20264 min read
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.
Lab1 Oct 20264 min read