Parallel radix sort in Python
Radix sort whose per-digit counting and scatter steps use parallel prefix sums.
- File
parallel_radix_sort.py, 77 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-radix-sort/. The authors are listed in prov/GHWWJ6.jsonl.
"""Parallel LSD radix sort with p blocks, simulated one block at a time.
Each pass counts digits per block, takes one prefix sum over the count
table in digit-major, block-minor order, and lets every block scatter
its keys to their final places. The blocks touch disjoint cells, so
running them one after the other gives the same result as running them
at the same time.
"""
def block_bounds(n: int, p: int, j: int) -> tuple[int, int]:
"""The index range of block j when n keys are cut into p blocks."""
return j * n // p, (j + 1) * n // p
def digit(x: int, t: int, radix: int) -> int:
"""Digit t of x in base radix, with digit 0 the least significant."""
return (x // radix**t) % radix
def radix_pass(a: list[int], t: int, radix: int, p: int) -> list[int]:
"""One stable counting-sort pass on digit t, with p blocks."""
n = len(a)
counts = [[0] * p for _ in range(radix)]
for j in range(p): # @line L2
lo, hi = block_bounds(n, p, j)
for i in range(lo, hi): # @line L3
counts[digit(a[i], t, radix)][j] += 1
offset = [[0] * p for _ in range(radix)]
total = 0
for d in range(radix): # @line L4
for j in range(p):
offset[d][j] = total
total += counts[d][j]
out = [0] * n
for j in range(p): # @line L5
lo, hi = block_bounds(n, p, j)
for i in range(lo, hi): # @line L6
d = digit(a[i], t, radix) # @line L7
out[offset[d][j]] = a[i]
offset[d][j] += 1
return out # @line L8
def parallel_radix_sort(a: list[int], radix: int = 256, p: int = 4) -> list[int]:
"""Return the non-negative integers of a in sorted order."""
if any(x < 0 for x in a):
raise ValueError("keys must be non-negative")
if radix < 2 or p < 1:
raise ValueError("need radix >= 2 and p >= 1")
passes = 1
while a and radix**passes <= max(a):
passes += 1
out = list(a)
for t in range(passes): # @line L1
out = radix_pass(out, t, radix, p)
return out
def split_radix_sort(a: list[int], bits: int) -> list[int]:
"""Blelloch's split radix sort: one stable split by each bit, low bit first."""
out = list(a)
for t in range(bits):
flags = [(x >> t) & 1 for x in out]
zeros = len(out) - sum(flags)
ones_before, pos = 0, [0] * len(out)
for i, f in enumerate(flags): # a prefix sum of the flags
pos[i] = zeros + ones_before if f else i - ones_before
ones_before += f
nxt = [0] * len(out)
for i, x in enumerate(out):
nxt[pos[i]] = x
out = nxt
return out
test_parallel_radix_sort.py 54 lines
# SPDX-License-Identifier: MIT
# Part of https://algorithm.tamnd.com/parallel-radix-sort/. The authors are listed in prov/GHWWJ6.jsonl.
import random
import pytest
from parallel_radix_sort import parallel_radix_sort, radix_pass, split_radix_sort
EXAMPLE = [170, 45, 75, 90, 802, 24, 2, 66]
def test_example_passes() -> None:
first = radix_pass(EXAMPLE, 0, 10, 2)
assert first == [170, 90, 802, 2, 24, 45, 75, 66]
second = radix_pass(first, 1, 10, 2)
assert second == [802, 2, 24, 45, 66, 170, 75, 90]
assert radix_pass(second, 2, 10, 2) == sorted(EXAMPLE)
def test_example() -> None:
assert parallel_radix_sort(EXAMPLE, 10, 2) == sorted(EXAMPLE)
assert split_radix_sort(EXAMPLE, 10) == sorted(EXAMPLE)
def test_edges() -> None:
assert parallel_radix_sort([]) == []
assert parallel_radix_sort([0]) == [0]
assert parallel_radix_sort([3, 3, 3], 2, 5) == [3, 3, 3]
with pytest.raises(ValueError):
parallel_radix_sort([-1])
def test_pass_is_stable() -> None:
a = [13, 23, 3, 41, 11, 33, 1]
assert radix_pass(a, 0, 10, 3) == [41, 11, 1, 13, 23, 3, 33]
def test_block_count_does_not_change_result() -> None:
rng = random.Random(4)
a = [rng.randrange(10**6) for _ in range(500)]
want = sorted(a)
for p in (1, 2, 3, 7, 64, 500, 600):
for radix in (2, 10, 16, 256):
assert parallel_radix_sort(a, radix, p) == want
def test_random_against_sorted() -> None:
rng = random.Random(12)
for _ in range(200):
a = [rng.randrange(1 << 16) for _ in range(rng.randint(0, 60))]
assert parallel_radix_sort(a, 16, rng.randint(1, 9)) == sorted(a)
assert split_radix_sort(a, 16) == sorted(a)
What the next level needs
- the tests did not run in this build