CSE3144 Β· Advanced Data Structures Β· Jul–Nov Semester 2026 Β· Lecture 4 of 36 Β· CO CSE3144.1

External Sorting & the Memory Hierarchy

What happens to sorting when the data is bigger than your memory β€” and why the answer is "stop counting comparisons, start counting disk blocks."

Dr. Manu ShrivastavaCourse Instructor Β· Consultation Fri 2–5 PM, LHC 308F
40 minutesSession outcome: understand external sorting and disk-based processing
L4 Β· 00 β€” Agenda 40 min total

Today, minute by minute

The problem β€” and why your fast sorts break

00–05

Lecture 2's promise comes due: 100 GB of data, 8 GB of RAM.

The memory hierarchy, with real numbers

05–11

Registers to disk; seek, latency, transmission; the block; the golden rule.

The idea: external merge sort in two phases

11–16

Run generation + merging. Why merge is the one primitive disks love.

Full trace β€” 13 records, memory of 3

16–22

Every run, every pass, every merge shown.

The classic analysis β€” 4,500 records

22–30

750-record memory, 250-record blocks: derive the full cost formula.

Counting passes; why k-way merging wins

30–35

passes = logk m, and each pass rereads everything.

Real systems + recap + homework

35–40

The 900 MB / 100 MB recipe; Unix sort; database ORDER BY.

CSE3144 β€” Lecture 4
L4 Β· 01 β€” The problem ~5 min

Lecture 2's IOU comes due

In Lecture 2 we said basic structures silently assume data fits in memory, and promised a fix. Here it is. The setting: a file of records on disk, far larger than RAM. Sort it.

Why not just run quicksort?

Because the array isn't in memory β€” only a window of it is

Quicksort, heapsort, and friends assume O(1) random access: touch element 7, then element 4,000,000, then element 12 β€” all equally cheap. On RAM, true. But if the "array" lives on disk, every jump to a far-away element is a disk seek costing ~10 ms. A quicksort partition pass over 100 GB makes hundreds of millions of scattered accesses.

The OS will try to hide this with paging β€” loading 4 KB pages in and out of RAM β€” and the result is thrashing: the machine spends its life moving pages, not sorting. Days, not hours.

Everyday analogy β€” carry it through the lecture

Grading 4,500 answer sheets on a small desk

You must arrange 4,500 answer sheets by roll number. The sheets are in the store room (disk). Your desk (RAM) holds only 750 sheets at a time. Walking to the store room is slow, and each trip you carry a fixed bundle of 250 sheets (a block).

You cannot "quicksort" 4,500 sheets on a 750-sheet desk. But you can: bring 750, sort them on the desk, tie them into a sorted bundle, return it β€” repeat 6 times. Then merge the 6 sorted bundles, carrying only the top few sheets of each to the desk. That, exactly, is external sorting.

Definitions. Internal sorting β€” all records in main memory throughout (bubble, insertion, quick, heap, merge…). External sorting β€” records live on secondary storage; only chunks visit memory. Different game, different rules, different cost model.

CSE3144 β€” Lecture 4
L4 Β· 02 β€” The memory hierarchy ~6 min

Know your ladder β€” and what each rung costs

OperationActual timeHuman scale (1 ns β†’ 1 s)
L1 cache reference0.5 nshalf a second
Main memory (RAM) reference100 ns~1.5 minutes
Read 4 KB randomly from SSD150 Β΅s~1.7 days
Read 1 MB sequentially from RAM250 Β΅s~3 days
Disk seek (HDD)10 ms~4 months
Read 1 MB sequentially from HDD30 ms~1 year

Canonical figures ("latency numbers every programmer should know," Jeff Dean). Exact values drift with hardware; the ratios are what matter: RAM vs disk seek is a factor of ~100,000.

Anatomy of one disk access β€” three costs (from the text)
  • Seek time (ts) β€” move the read/write head to the right cylinder. The big one: ~10 ms.
  • Latency time (tl) β€” wait for the platter to rotate the right sector under the head.
  • Transmission time (trw) β€” actually stream the block of data.

So one input/output costs tIO = ts + tl + trw. Seek + latency are pure overhead paid per access, not per byte β€” which is why disks reward few, large, sequential accesses and punish many, small, random ones.

The block: the unit of data read or written to disk in one go (one "bundle of sheets"). You can never fetch one record; you always fetch its whole block.

The golden rule of this lecture

When data lives on disk, stop counting comparisons β€” count block I/Os. A comparison costs nanoseconds; a block access costs milliseconds. One disk access buys you time for roughly a million comparisons. Every design decision in external sorting is an attempt to do fewer I/Os, and to make the unavoidable ones sequential.

CSE3144 β€” Lecture 4
L4 Β· 03 β€” The idea ~5 min

External merge sort: two phases, both sequential

Phase 1 β€” Run generation (the sort phase)

Read as many records as memory holds (M records), sort them internally with any good in-memory sort (quicksort, heapsort), and write the sorted chunk back to disk. Each sorted chunk is called a run. Repeat until the whole file is consumed.

while file not exhausted:
    read M records          // sequential read
    sort internally         // free! (vs I/O)
    write sorted run        // sequential write

Result: ⌈n/MβŒ‰ sorted runs on disk. Nothing here ever jumps around the disk β€” every read and write is a straight stream.

Phase 2 β€” Merging (why merge, of all things?)

Because merging two sorted lists only ever looks at the front of each list. To merge two runs of 750 records each, you don't need 1,500 records in memory β€” you need one block of each plus an output block:

  • 2 input buffers β€” hold the current block of each run,
  • 1 output buffer β€” collects merged records; written out when full,
  • input buffer empties β†’ refill with that run's next block.

Memory needed: 3 blocks, regardless of run length. Every block of every run is read once, sequentially. Merge is the only classic sort primitive with this property β€” that's why external sorting is always merge-based.

Merging pairs of runs doubles run length each pass: 6 runs β†’ 3 β†’ 2 β†’ 1. When one run remains, the file is sorted. This is 2-way external merge sort, today's workhorse; k-way (merging k runs at once) is the upgrade we'll motivate at the end and build properly next lecture.

Interactive refresher β€” merge two sorted runs, one step at a time

Two sorted runs, one finger (amber outline) on the front of each. Each step: compare the fingers, copy the smaller (teal) to the output, advance that finger. Drive it yourself:

Run A
Run B
Output C

The property to notice: each element is read once, in order β€” no finger ever moves backwards. That is what lets each run stream from disk block by block, sequentially.

CSE3144 β€” Lecture 4
L4 Β· 04 β€” Full trace ~6 min

Watch it work: 13 records, desk of size M = 3

Unsorted file on disk (13 records): 81, 94, 11, 96, 12, 35, 17, 99, 28, 58, 41, 75, 15. Memory holds only 3 records at a time.

Interactive trace β€” drive both phases yourself

Legend: amber = being processed now Β· teal = just written to disk Β· faded = already consumed Β· dotted = idle, waiting for a partner.

Question to the class (30 seconds)

The lone run 15 was read and written in every pass while waiting for its turn. Wasteful? Could we have scheduled the merges differently so short runs are handled cheaply? Hold the thought β€” that is exactly the optimal merging of runs problem, and it leads to Huffman trees in Lecture 6.

What was in memory at any moment?

Never more than 3 records' worth of blocks: two input fingers and an output collector. The file could have had 13 billion records β€” the memory bill for 2-way merging stays 3 blocks. Memory bounds the run size and merge width, never the file size.

CSE3144 β€” Lecture 4
L4 Β· 05 β€” The classic analysis ~8 min

4,500 records, 750-record memory, 250-record blocks

The classic textbook example (it appears in university question papers almost every year). Setup: file of 4,500 records on disk; internal memory sorts at most 750; one block = 250 records, so memory holds 3 blocks and the file spans 18 blocks.

Phase 1 β€” six runs

Read 3 blocks (750 records) at a time, sort internally (heapsort/quicksort), write back:

R1: 1–750R2: 751–1500R3: …R4: …R5: …R6: 3751–4500

6 runs of 750 records (3 blocks) each. I/O: 18 block-reads + 18 block-writes, plus 6 internal sorts.

Phase 2 β€” memory as three 250-record buffers

Split the 750-record memory into two input buffers + one output buffer (250 each). Merge R1+R2 block by block: read one block of each run; merge into the output buffer; when it fills, write it out; when an input buffer drains, refill it from the same run. Then R3+R4, then R5+R6. Result of pass 1: three runs of 1,500. Pass 2: merge two of them β†’ 3,000 (the third 1,500-run sits idle). Pass 3: 3,000 + 1,500 β†’ 4,500. Done.

Interactive β€” watch the runs evolve and the bill grow

The same analysis as a formal table, with proper notation β€” ts = max seek, tl = max latency, trw = read/write one block, tIO = ts + tl + trw, tIS = internally sort 750 records, nΒ·tm = merge n records buffer-to-buffer:

StepOperationWhy that countTime
1Run generationread 18 blocks + write 18 blocks + sort 6 chunks36 tIO + 6 tIS
2Pass 1: merge R1–R6 in pairswhole file moves: 18 in + 18 out; all 4,500 records pass through the merger36 tIO + 4500 tm
3Pass 2: merge two 1,500-runsonly 3,000 records move = 12 blocks in + 12 out β€” the idle run costs nothing24 tIO + 3000 tm
4Pass 3: 3,000 + 1,500whole file again: 18 + 18 blocks36 tIO + 4500 tm
Total132 tIO + 12000 tm + 6 tIS
CSE3144 β€” Lecture 4
L4 Β· 06 β€” Counting passes ~5 min

Every pass rereads everything β€” so count passes

The formulas (m runs, k-way merge)
levels = logk m + 1   Β·   passes = logk m

Check against what we've seen (2-way, m = 6): logβ‚‚ 6 β‰ˆ 2.58 β†’ 3 passes, 4 levels β€” matches the 4,500-record example. And each pass costs β‰ˆ one full read + one full write of the file (36 tIO there).

So total merge I/O β‰ˆ 2 Γ— (file blocks) Γ— logk m. The file size is fixed; the only lever you own is the number of passes.

Pull the lever yourself: 16 runs, pick your k

Merging k runs at once means the whole file is touched only logk m times. The dream: enough memory buffers to merge all runs in a single pass.

So why not k = 1000? The two catches (previews of Lecture 5)
  • Catch 1 β€” memory: a k-way merge needs k input buffers + 1 output buffer minimum (and 2k + 2 for smooth parallel I/O β€” see next lecture's floating buffers). Buffers shrink as k grows, so each holds fewer records, forcing more frequent refills. k is capped by memory.
  • Catch 2 β€” CPU per record: naively finding the minimum among k fronts costs k comparisons per record output. Fix: a tournament / loser tree finds the next minimum in logβ‚‚ k comparisons β€” the star of Lecture 5. With it, internal merge time is O(n logβ‚‚ k Β· logk m) = O(n logβ‚‚ m), independent of k β€” so raising k costs the CPU nothing and saves real I/O.
CSE3144 β€” Lecture 4
L4 Β· 07 β€” A real recipe ~4 min

900 MB of data, 100 MB of RAM β€” start to finish

The recipe (single merge pass)
  • Step 1: Read 100 MB, quicksort it in RAM, write it out as a run. Repeat β€” 900/100 = 9 runs on disk.
  • Step 2: Divide RAM into 10 buffers of 10 MB: 9 input buffers (one per run) + 1 output buffer.
  • Step 3: Load the first 10 MB of each run; do a 9-way merge. Output buffer fills β†’ append to the final file. An input buffer drains β†’ refill with that run's next 10 MB.

Because 9 runs fit the buffer budget, one merge pass suffices: log₉ 9 = 1.

Total the I/O β€” and appreciate it
StageI/O
Run generation: read + write 900 MB1.8 GB
Merge pass: read + write 900 MB1.8 GB
Total, all sequential3.6 GB

At ~150 MB/s sequential HDD speed that's ~24 seconds of I/O. The thrashing quicksort alternative makes random 4 KB page accesses β€” at 10 ms each, even one access per 4 KB of data would take ~38 minutes' worth of seeks (900 MB Γ· 4 KB β‰ˆ 230,400 accesses Γ— 10 ms). Same machine, same data: the difference is purely access pattern.

Where you already use this β€” every day

The Unix/Linux sort command is an external merge sort (watch it drop temporary run files in /tmp on a big input). PostgreSQL / MySQL: an ORDER BY or index build that exceeds work_mem "spills to disk" β€” run generation and merging, verbatim. MapReduce / Spark: the shuffle phase between map and reduce is a giant distributed external merge sort. DuckDB, SQLite β€” same story. This lecture's algorithm is running in every serious data system you will ever touch.

CSE3144 β€” Lecture 4
L4 Β· 08 β€” Recap & what's next ~3 min

Three takeaways, one cliffhanger

Lecture 5

Tournament Trees, Buffering & Run Generation

The loser tree that finds the min in log k; floating buffers that keep disk and CPU busy simultaneously; replacement selection β€” runs 2Γ— longer than memory.

Lecture 6

Huffman Trees

Merging unequal runs in the cheapest order β€” today's "idle run 15" question answered optimally, via weighted external path length.

Homework β€” bring solutions to Lecture 5
  • 10,000 records, memory sorts 1,000, block = 100 records. How many runs does Phase 1 produce? How many passes for 2-way merging? For 5-way? Compute total block I/Os for both.
  • Re-derive the 4,500-record cost table with a 4,500-record file but 500-record internal memory and 250-record blocks. Which term grows, and why?
  • In pass 2 of the classic example, we merged the two 1,500-runs and let the third idle. Would merging (R + idle) differently change the total? Try all orders of merging runs of sizes 1500, 1500, 1500 β€” then sizes 750, 1500, 2250. What do you conjecture? (Lecture 6 will prove it.)
  • On any Linux machine: seq 1 50000000 | shuf > big.txt; sort -n big.txt > sorted.txt β€” and watch /tmp while it runs. Report what you see.
  • Reading: Weiss ch. 7 (external sorting); CLRS ch. 2 (the merge subroutine).
CSE3144 β€” Lecture 4

Questions?

Dr. Manu Shrivastava β€” LHC 308F β€” Friday 2:00–5:00 PM

Next: Lecture 5 β€” Tournament Trees, Buffering, and Run Generation.