ShopperCove
Menu
All writingBlogTopicsCategoriesAboutRSS
Blog
Categories
Observability & SRE62All categories
About

Plate 07

  1. Blog
  2. /Observability & SRE

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 Challa·1 October 2026·4 min read

Lab
On this page
  1. Intro — what this post promises
  2. Arms
  3. Lab topology
  4. Lead table — k=4 × 25 k (p50 Mrec/s)
  5. Scale sketch (merge→list vs sorted(chain))
  6. When merge still wins the design
  7. Memory shape
  8. Reading it
  9. Early-stop pattern
  10. Integer-run caveat
  11. Pitfalls
  12. Reproduce
  13. Limits
  14. Takeaway

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

ArmPattern
list(heapq.merge(*runs))multi-way merge → list
sorted(chain.from_iterable)flatten then timsort
sorted(chain(*runs))same, star form
merge / sorted consumeiterate results
chain consume onlyno sort (baseline)

Lab topology

configs: k4×25k · k8×25k · k16×10k · k4×100k · 7 rounds · p50
metric: records/s = total / p50_s

Script: lab-evidence/108-heapq-merge-vs-sorted/results/run_lab.py.


Lead table — k=4 × 25 k (p50 Mrec/s)

ArmMrec/s
sorted(chain.from_iterable)85.15
sorted(chain(*runs))82.85
chain consume (unsorted)54.38
sorted consume36.9
heapq.merge → list9.82
heapq.merge consume8.54

Timsort on already-structured int runs is extremely fast; merge pays heap maintenance per output.


Scale sketch (merge→list vs sorted(chain))

Configheapq.mergesorted(chain)
k4 × 25k9.8285.15
k8 × 25k7.8366.99
k16 × 10k6.6954.62
k4 × 100k9.8376.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 merge on unsorted inputs (silent wrong order).
  • Building list(merge(...)) then wondering why it is slower than sorted.
  • Forgetting early-stop with islice when merge’s laziness is the point.
  • Comparing to nlargest / single-heap patterns.

Reproduce

python3 lab-evidence/108-heapq-merge-vs-sorted/results/run_lab.py

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.

heapq.mergesorted chainmulti-way mergeitertools.chainpython heapqlocalhost labsrerecords/s

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.

Notes when a lab post goes up

Occasional email for new hands-on reviews. No sequence and no sponsors.

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

On this page

  1. Intro — what this post promises
  2. Arms
  3. Lab topology
  4. Lead table — k=4 × 25 k (p50 Mrec/s)
  5. Scale sketch (merge→list vs sorted(chain))
  6. When merge still wins the design
  7. Memory shape
  8. Reading it
  9. Early-stop pattern
  10. Integer-run caveat
  11. Pitfalls
  12. Reproduce
  13. Limits
  14. Takeaway
All writingBlogCategoriesTopicsAboutPrivacyRSS

© 2026 ShopperCove