Parallel merge sort in Python
Merge sort in which the two recursive calls and the merge run in parallel.
- File
parallel_merge_sort.py, 89 lines- Level
- I0 not checked yet
- Toolchain
- Python 3.14, pytest 9
- Test framework
- pytest
- Run the tests
python3 -m pytest
# SPDX-License-Identifier: MIT
# Part of https://algorithm.tamnd.com/parallel-merge-sort/. The authors are listed in prov/J1JZCG.jsonl.
"""Parallel merge sort in the work-span model.
The code runs the spawned calls one after the other, which is a legal
schedule of the parallel program. Each function also returns the work
and the span of the computation in a unit-cost model: one unit for each
call, each probe of a binary search and each copied item.
"""
from typing import Protocol, TypeVar
class Ordered(Protocol):
def __lt__(self, other: "Ordered", /) -> bool: ...
T = TypeVar("T", bound=Ordered)
def lower_bound(y: list[T], lo: int, hi: int, key: T) -> tuple[int, int]:
"""First index r in [lo, hi] with not y[r] < key, and the number of probes."""
probes = 0
while lo < hi:
probes += 1
mid = (lo + hi) // 2
if y[mid] < key:
lo = mid + 1
else:
hi = mid
return lo, probes
def upper_bound(x: list[T], lo: int, hi: int, key: T) -> tuple[int, int]:
"""First index r in [lo, hi] with key < x[r], and the number of probes."""
probes = 0
while lo < hi:
probes += 1
mid = (lo + hi) // 2
if key < x[mid]:
hi = mid
else:
lo = mid + 1
return lo, probes
def p_merge(x: list[T], a1: int, a2: int, y: list[T], b1: int, b2: int, z: list[T], c: int) -> tuple[int, int]:
"""Merge sorted x[a1:a2] and y[b1:b2] into z[c:], stably (x first on ties).
Return (work, span)."""
na, nb = a2 - a1, b2 - b1 # @line L8
if na + nb == 0: # @line L9
return 1, 1
if na >= nb: # @line L10
q = a1 + na // 2 # @line L11
r, probes = lower_bound(y, b1, b2, x[q]) # @line L12
t = c + (q - a1) + (r - b1) # @line L13
z[t] = x[q] # @line L14
w1, s1 = p_merge(x, a1, q, y, b1, r, z, c) # @line L15
w2, s2 = p_merge(x, q + 1, a2, y, r, b2, z, t + 1) # @line L16
# sync: the two calls above are done here # @line L17
else: # @line L18
q = b1 + nb // 2 # @line L19
r, probes = upper_bound(x, a1, a2, y[q]) # @line L20
t = c + (r - a1) + (q - b1) # @line L21
z[t] = y[q] # @line L22
w1, s1 = p_merge(x, a1, r, y, b1, q, z, c) # @line L23
w2, s2 = p_merge(x, r, a2, y, q + 1, b2, z, t + 1) # @line L24
# sync: the two calls above are done here # @line L25
return 1 + probes + w1 + w2, 1 + probes + max(s1, s2)
def p_merge_sort(a: list[T], lo: int = 0, hi: int | None = None, buf: list[T] | None = None) -> tuple[int, int]:
"""Sort a[lo:hi] in place, stably. Return (work, span)."""
if hi is None:
hi = len(a)
if buf is None:
buf = list(a)
if hi - lo <= 1: # @line L1
return 1, 1
mid = lo + (hi - lo) // 2 # @line L2
w1, s1 = p_merge_sort(a, lo, mid, buf) # @line L3
w2, s2 = p_merge_sort(a, mid, hi, buf) # @line L4
# sync: both halves are sorted here # @line L5
buf[lo:hi] = a[lo:hi] # @line L6: a parallel loop, span about lg(hi - lo)
copy_span = (hi - lo).bit_length()
w3, s3 = p_merge(buf, lo, mid, buf, mid, hi, a, lo) # @line L7
return 1 + w1 + w2 + (hi - lo) + w3, 1 + max(s1, s2) + copy_span + s3
test_parallel_merge_sort.py 62 lines
# SPDX-License-Identifier: MIT
# Part of https://algorithm.tamnd.com/parallel-merge-sort/. The authors are listed in prov/J1JZCG.jsonl.
import math
import random
from parallel_merge_sort import p_merge, p_merge_sort
def test_example() -> None:
a = [38, 27, 43, 3, 9, 82, 10]
p_merge_sort(a)
assert a == [3, 9, 10, 27, 38, 43, 82]
def test_merge_example() -> None:
x, y = [3, 27, 38, 43], [9, 10, 82]
z = [0] * 7
p_merge(x, 0, 4, y, 0, 3, z, 0)
assert z == [3, 9, 10, 27, 38, 43, 82]
def test_edges() -> None:
for a in ([], [1], [2, 1], [1, 1, 1], [3, 2, 1], [1, 2, 3]):
b = list(a)
p_merge_sort(b)
assert b == sorted(a)
def test_stable() -> None:
class Key:
def __init__(self, k: int, tag: str) -> None:
self.k, self.tag = k, tag
def __lt__(self, other: "Key") -> bool:
return self.k < other.k
a = [Key(2, "a"), Key(1, "b"), Key(2, "c"), Key(1, "d"), Key(2, "e"), Key(1, "f")]
p_merge_sort(a)
assert [x.tag for x in a] == ["b", "d", "f", "a", "c", "e"]
def test_merge_work_is_linear_and_span_polylog() -> None:
rng = random.Random(3)
for n in (1, 2, 5, 64, 1000, 4096):
keys = sorted(rng.sample(range(10 * n + 10), 2 * n))
x, y = keys[0::2], keys[1::2]
z = [0] * (2 * n)
work, span = p_merge(x, 0, n, y, 0, n, z, 0)
assert z == keys
big = 2 * n
assert work <= 8 * big + 2
assert span <= (math.log(big, 4 / 3) + 2) * (math.log2(big) + 2)
def test_random_against_sorted() -> None:
rng = random.Random(11)
for _ in range(300):
a = [rng.randint(-30, 30) for _ in range(rng.randint(0, 70))]
b = list(a)
p_merge_sort(b)
assert b == sorted(a)
What the next level needs
- the tests did not run in this build