Sklansky adder in Python
Parallel-prefix adder with logarithmic depth and high fan-out at each level.
- File
sklansky_adder.py, 108 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/sklansky-adder/. The authors are listed in prov/JA4QR9.jsonl.
"""The Sklansky (divide-and-conquer) parallel-prefix adder, simulated gate by gate.
Bit lists are least significant bit first. Every signal is a pair (value,
depth), and a Stats object counts gates, prefix cells and the fan-out of every
prefix signal, so that the size, depth and fan-out of the circuit can be checked.
"""
from collections import Counter
class Stats:
def __init__(self) -> None:
self.gates = 0
self.cells = 0
self.fanout: Counter = Counter() # uses of the pair held at a position, keyed by (position, version)
self.max_recursion = 0
def _gate(op, x: tuple[int, int], y: tuple[int, int], st: Stats) -> tuple[int, int]:
st.gates += 1
return op(x[0], y[0]), max(x[1], y[1]) + 1
def _and(x, y, st):
return _gate(lambda u, v: u & v, x, y, st)
def _or(x, y, st):
return _gate(lambda u, v: u | v, x, y, st)
def _xor(x, y, st):
return _gate(lambda u, v: u ^ v, x, y, st)
def _prefix(G: list, P: list, version: list[int], lo: int, hi: int, st: Stats, depth: int) -> None:
"""After the call, (G[i], P[i]) is the pair of the block lo..i for lo <= i < hi."""
st.max_recursion = max(st.max_recursion, depth)
if hi - lo <= 1: # @line L7
return
mid = lo + (hi - lo + 1) // 2 # @line L8: the low half has ceil(size / 2) positions
_prefix(G, P, version, lo, mid, st, depth + 1) # @line L9: the two halves, in parallel
_prefix(G, P, version, mid, hi, st, depth + 1)
gl, pl = G[mid - 1], P[mid - 1]
for i in range(mid, hi): # @line L10: combine every high position with the top of the low half
st.fanout[(mid - 1, version[mid - 1])] += 1
G[i] = _or(G[i], _and(P[i], gl, st), st)
P[i] = _and(P[i], pl, st)
version[i] += 1
st.cells += 1
def sklansky_add(a: list[int], b: list[int], c_in: int = 0, st: Stats | None = None) -> tuple[list[int], int]:
"""Add two n-bit lists; return the n + 1 sum bits (carry out last) and the depth."""
if st is None:
st = Stats()
n = len(a)
if len(b) != n:
raise ValueError("the operands must have the same width")
cin = (c_in & 1, 0)
if n == 0:
return [cin[0]], 0
A = [(x & 1, 0) for x in a]
B = [(x & 1, 0) for x in b]
g = [_and(A[i], B[i], st) for i in range(n)] # @line L1: generate and propagate bits
p = [_xor(A[i], B[i], st) for i in range(n)]
g[0] = _or(g[0], _and(p[0], cin, st), st) # @line L2: fold the carry in
G, P = list(g), list(p)
_prefix(G, P, [0] * n, 0, n, st, 0) # @line L3
c = [cin] + G # @line L4: c[i + 1] = G[i]
s = [_xor(p[i], c[i], st) for i in range(n)] # @line L5
out = s + [c[n]]
return [x[0] for x in out], max(x[1] for x in out) # @line L6
def prefix_blocks(n: int) -> list[list[tuple[int, int]]]:
"""The block lo..i held by each position after each level, for the example table."""
levels = []
size = 1
while size < n:
size *= 2
levels.append([((i // size) * size, i) for i in range(n)])
return levels
def to_bits(x: int, n: int) -> list[int]:
return [(x >> i) & 1 for i in range(n)]
def from_bits(s: list[int]) -> int:
return sum(bit << i for i, bit in enumerate(s))
def add_limbs(a: list[int], b: list[int], w: int = 8, c_in: int = 0) -> list[int]:
"""Add two limb arrays in radix 2**w with one Sklansky adder of their full width."""
n = max(len(a), len(b))
a = list(a) + [0] * (n - len(a))
b = list(b) + [0] * (n - len(b))
abits = [(a[i // w] >> (i % w)) & 1 for i in range(n * w)]
bbits = [(b[i // w] >> (i % w)) & 1 for i in range(n * w)]
s, _ = sklansky_add(abits, bbits, c_in)
out = [0] * (n + 1)
for i, bit in enumerate(s):
out[i // w] |= bit << (i % w)
return out
test_sklansky_adder.py 93 lines
# SPDX-License-Identifier: MIT
# Part of https://algorithm.tamnd.com/sklansky-adder/. The authors are listed in prov/JA4QR9.jsonl.
import math
import random
import pytest
from sklansky_adder import Stats, add_limbs, from_bits, prefix_blocks, sklansky_add, to_bits
def limbs(x: int, w: int, n: int) -> list[int]:
return [(x >> (w * i)) & ((1 << w) - 1) for i in range(n)]
def value(d: list[int], w: int) -> int:
v = 0
for x in reversed(d):
v = (v << w) | x
return v
def cells(n: int) -> int:
if n <= 1:
return 0
lo = (n + 1) // 2
return cells(lo) + cells(n - lo) + (n - lo)
def test_example() -> None:
s, depth = sklansky_add(to_bits(182, 8), to_bits(91, 8))
assert from_bits(s) == 273
assert depth == 10
assert prefix_blocks(8)[0][5] == (4, 5)
assert prefix_blocks(8)[1][6] == (4, 6)
assert prefix_blocks(8)[2][6] == (0, 6)
def test_size_depth_fanout() -> None:
for n in range(1, 131):
st = Stats()
s, depth = sklansky_add([1] * n, [0] * n, 1, st)
assert from_bits(s) == 1 << n
L = math.ceil(math.log2(n)) if n > 1 else 0
assert st.cells == cells(n)
assert st.cells <= (n * L) // 2 + 1
assert depth <= 2 * L + 4
assert st.max_recursion == L
assert (max(st.fanout.values()) if st.fanout else 0) == n // 2
if n & (n - 1) == 0:
assert st.cells == (n // 2) * L
def test_exhaustive_small() -> None:
for n in (1, 2, 3, 4, 5):
for x in range(1 << n):
for y in range(1 << n):
for c in (0, 1):
s, _ = sklansky_add(to_bits(x, n), to_bits(y, n), c)
assert from_bits(s) == x + y + c
def test_edges() -> None:
assert sklansky_add([], [], 1)[0] == [1]
for n in (1, 7, 8, 9, 31, 64):
top = (1 << n) - 1
for x, y, c in [(0, 0, 0), (top, 0, 1), (top, 1, 0), (top, top, 1), (top, top, 0), (top >> 1, 1, 0)]:
s, _ = sklansky_add(to_bits(x, n), to_bits(y, n), c)
assert from_bits(s) == x + y + c
with pytest.raises(ValueError):
sklansky_add([1, 0], [1], 0)
def test_random_limbs() -> None:
rng = random.Random(1960)
for w in (4, 8):
for _ in range(300):
n = rng.randint(1, 12)
x = rng.getrandbits(n * w)
y = rng.getrandbits(n * w)
c = rng.randint(0, 1)
out = add_limbs(limbs(x, w, n), limbs(y, w, n), w, c)
assert len(out) == n + 1
assert value(out, w) == x + y + c
def test_wide() -> None:
rng = random.Random(11)
for _ in range(20):
n = rng.randint(200, 1000)
x, y = rng.getrandbits(n), rng.getrandbits(n)
s, _ = sklansky_add(to_bits(x, n), to_bits(y, n), 0)
assert from_bits(s) == x + y
What the next level needs
- the tests did not run in this build