Booth's multiplication in Python
Signed multiplication that recodes the multiplier to reduce the number of nonzero partial products.
- File
booth_multiplication.py, 99 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/booth-multiplication/. The authors are listed in prov/B6TGDA.jsonl.
"""Booth's multiplication of n-bit two's complement integers.
Registers are fixed-width bit patterns held in Python ints in [0, 2^width).
booth_multiply is the sequential radix-2 algorithm with the registers A, Q and
the extra bit q_{-1}; booth4_multiply sums the radix-4 recoded partial products.
"""
def to_unsigned(x: int, width: int) -> int:
"""The width-bit two's complement pattern of x."""
return x & ((1 << width) - 1)
def to_signed(u: int, width: int) -> int:
"""The value of a width-bit two's complement pattern."""
return u - (1 << width) if u >> (width - 1) & 1 else u
def _check(x: int, y: int, n: int) -> None:
if n < 1:
raise ValueError("n must be at least 1")
lo, hi = -(1 << (n - 1)), (1 << (n - 1)) - 1
if not (lo <= x <= hi and lo <= y <= hi):
raise ValueError("operands must be n-bit two's complement integers")
def booth_multiply(x: int, y: int, n: int) -> tuple[int, int]:
"""Return (x * y, number of additions and subtractions) by radix-2 Booth.
Precondition: x and y are n-bit two's complement integers, n >= 1.
A has n + 1 bits so that A - x cannot overflow; (A, Q) holds 2n + 1 bits
and the product is their low 2n bits read as a two's complement number.
Cost: n steps, each one add or subtract of n + 1 bits and a shift.
"""
_check(x, y, n)
wa = n + 1
mask_a = (1 << wa) - 1
xa = to_unsigned(x, wa)
a, q, q_1 = 0, to_unsigned(y, n), 0 # @line L1
ops = 0
for _ in range(n): # @line L2
q0 = q & 1
if (q0, q_1) == (1, 0): # @line L3
a = (a - xa) & mask_a
ops += 1
elif (q0, q_1) == (0, 1): # @line L4
a = (a + xa) & mask_a
ops += 1
q_1 = q0 # @line L5
q = (q >> 1) | ((a & 1) << (n - 1))
a = (a >> 1) | (a & (1 << (wa - 1))) # arithmetic shift keeps the sign
product = (a << n) | q # @line L6
return to_signed(product & ((1 << (2 * n)) - 1), 2 * n), ops
def booth2_digits(y: int, n: int) -> list[int]:
"""Radix-2 Booth digits y_{i-1} - y_i, i = 0..n-1, of an n-bit y."""
u = to_unsigned(y, n)
bits = [(u >> i) & 1 for i in range(n)]
return [(bits[i - 1] if i else 0) - bits[i] for i in range(n)]
def booth4_digits(y: int, n: int) -> list[int]:
"""Radix-4 Booth digits y_{2j-1} + y_{2j} - 2 y_{2j+1}, in {-2..2}.
The pattern is sign-extended to an even number of bits first.
"""
m = n + (n & 1)
u = to_unsigned(y, m)
bits = [(u >> i) & 1 for i in range(m)]
digits = []
for j in range(m // 2): # @line L7
low = bits[2 * j - 1] if j else 0
digits.append(low + bits[2 * j] - 2 * bits[2 * j + 1])
return digits
def booth4_multiply(x: int, y: int, n: int) -> tuple[int, int]:
"""Return (x * y, number of nonzero partial products) by radix-4 Booth.
Each partial product q_j * x is 0, +-x or +-2x: a shift and a negation
in 2n-bit two's complement, added with a 2n-bit adder.
"""
_check(x, y, n)
width = 2 * n + 2
mask = (1 << width) - 1
acc = 0
nonzero = 0
for j, q in enumerate(booth4_digits(y, n)): # @line L8
if q == 0:
continue
nonzero += 1
mag = to_unsigned(x << (abs(q) - 1), width)
term = (-mag) & mask if q < 0 else mag
acc = (acc + (term << (2 * j))) & mask
return to_signed(acc, width), nonzero # @line L9
test_booth_multiplication.py 84 lines
# SPDX-License-Identifier: MIT
# Part of https://algorithm.tamnd.com/booth-multiplication/. The authors are listed in prov/B6TGDA.jsonl.
import random
import pytest
from booth_multiplication import (
booth2_digits,
booth4_digits,
booth4_multiply,
booth_multiply,
to_signed,
to_unsigned,
)
def test_example():
# x = 3, y = -6 in 4 bits: y = 1010, digits (low first) 0, -1, +1, -1.
assert booth2_digits(-6, 4) == [0, -1, 1, -1]
assert booth_multiply(3, -6, 4) == (-18, 3)
assert booth4_digits(-6, 4) == [-2, -1]
assert booth4_multiply(3, -6, 4) == (-18, 2)
def test_run_of_ones():
# 0111 1110 has one run of ones: two operations instead of six.
assert booth_multiply(5, 0b01111110, 8) == (5 * 126, 2)
def test_digit_values():
for n in range(1, 9):
for y in range(-(1 << (n - 1)), 1 << (n - 1)):
d2 = booth2_digits(y, n)
assert sum(d << i for i, d in enumerate(d2)) == y
d4 = booth4_digits(y, n)
assert all(-2 <= d <= 2 for d in d4)
assert len(d4) == (n + 1) // 2
assert sum(d * 4**j for j, d in enumerate(d4)) == y
# nonzero radix-2 digits alternate in sign
nz = [d for d in d2 if d]
assert all(nz[i] == -nz[i + 1] for i in range(len(nz) - 1))
def test_exhaustive_small():
for n in range(1, 7):
r = range(-(1 << (n - 1)), 1 << (n - 1))
for x in r:
for y in r:
p, ops = booth_multiply(x, y, n)
assert p == x * y
assert ops == sum(1 for d in booth2_digits(y, n) if d)
p4, nz = booth4_multiply(x, y, n)
assert p4 == x * y
assert nz <= (n + 1) // 2
def test_random():
rng = random.Random(4242)
for _ in range(800):
n = rng.randint(1, 64)
lo, hi = -(1 << (n - 1)), (1 << (n - 1)) - 1
x, y = rng.randint(lo, hi), rng.randint(lo, hi)
assert booth_multiply(x, y, n)[0] == x * y
assert booth4_multiply(x, y, n)[0] == x * y
def test_worst_cases():
for n in (2, 4, 8, 16, 33):
lo, hi = -(1 << (n - 1)), (1 << (n - 1)) - 1
for x, y in ((lo, lo), (lo, hi), (hi, hi), (lo, -1), (-1, -1), (0, lo)):
assert booth_multiply(x, y, n)[0] == x * y
assert booth4_multiply(x, y, n)[0] == x * y
# alternating bits ...0101 give n operations in radix 2
alt = to_signed(int("01" * n, 2) & ((1 << n) - 1), n)
assert booth_multiply(1, alt, n)[1] == n
def test_conversions_and_errors():
assert to_signed(to_unsigned(-5, 8), 8) == -5
with pytest.raises(ValueError):
booth_multiply(8, 1, 4)
with pytest.raises(ValueError):
booth4_multiply(1, 1, 0)
What the next level needs
- the tests did not run in this build