Algorithm Search⌘K

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
sklansky_adder.pyRawGitHub
# 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

Read the full page about Sklansky adder