Karatsuba multiplication in Python
Divide-and-conquer multiplication using three half-size products, running in O(n^(log2 3)) time.
- File
karatsuba_multiplication.py, 168 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/karatsuba-multiplication/. The authors are listed in prov/QWY1MV.jsonl.
# Limb arrays are lists of ints in [0, 2^w), least significant limb first.
def _trim(a: list[int]) -> list[int]:
while a and a[-1] == 0:
a.pop()
return a
def _add_into(c: list[int], a: list[int], shift: int, w: int) -> None:
"""c += a * beta^shift, in place; c must be long enough for the result."""
mask = (1 << w) - 1
carry = 0
i = 0
for i, x in enumerate(a):
t = c[shift + i] + x + carry
c[shift + i] = t & mask
carry = t >> w
k = shift + len(a)
while carry:
t = c[k] + carry
c[k] = t & mask
carry = t >> w
k += 1
def _add(a: list[int], b: list[int], w: int) -> list[int]:
if len(a) < len(b):
a, b = b, a
c = list(a) + [0]
_add_into(c, b, 0, w)
return c
def _sub_in_place(c: list[int], a: list[int], w: int) -> None:
"""c -= a, in place, where c >= a as numbers."""
mask = (1 << w) - 1
borrow = 0
for i in range(len(c)):
t = c[i] - (a[i] if i < len(a) else 0) - borrow
borrow = 1 if t < 0 else 0
c[i] = t & mask
if i >= len(a) and not borrow:
break
if borrow:
raise ValueError("negative result")
def schoolbook(a: list[int], b: list[int], w: int, counter: list[int] | None = None) -> list[int]:
"""The base case: len(a) * len(b) limb products."""
mask = (1 << w) - 1
c = [0] * (len(a) + len(b))
for i, x in enumerate(a):
carry = 0
for j, y in enumerate(b):
t = c[i + j] + x * y + carry
c[i + j] = t & mask
carry = t >> w
c[i + len(b)] = carry
if counter is not None:
counter[0] += len(a) * len(b)
return c
def _sum_product(a0, a1, b0, b1, w, threshold, counter, mul) -> list[int]:
"""(a0 + a1)(b0 + b1) as 2h + 2 limbs, with one recursive product of h limbs.
a0 + a1 = s + ca beta^h with a carry bit ca, and the same for b, so
(a0 + a1)(b0 + b1) = s t + (ca t + cb s) beta^h + ca cb beta^(2h).
"""
h = len(a0)
sa, sb = _add(a0, a1, w), _add(b0, b1, w) # h + 1 limbs each
ca, cb = sa.pop(), sb.pop()
z = mul(sa, sb, w, threshold, counter) + [0, 0]
if ca:
_add_into(z, sb, h, w)
if cb:
_add_into(z, sa, h, w)
if ca and cb:
_add_into(z, [1], 2 * h, w)
return z
def karatsuba(a: list[int], b: list[int], w: int = 8, threshold: int = 4, counter: list[int] | None = None) -> list[int]:
"""Product of two limb arrays of the same length n, as 2n limbs.
Recursion: three products of about n/2 limbs, so depth about log2(n / threshold).
"""
n = len(a)
if n != len(b):
raise ValueError("karatsuba() needs operands of the same length")
if n <= max(threshold, 1): # @line L1
return schoolbook(a, b, w, counter)
h = (n + 1) // 2 # @line L2: a = a0 + a1 beta^h, b = b0 + b1 beta^h
a0, a1 = a[:h], a[h:] + [0] * (2 * h - n)
b0, b1 = b[:h], b[h:] + [0] * (2 * h - n)
z0 = karatsuba(a0, b0, w, threshold, counter) # @line L3
z2 = karatsuba(a1, b1, w, threshold, counter) # @line L4
z1 = _sum_product(a0, a1, b0, b1, w, threshold, counter, karatsuba) # @line L5
_sub_in_place(z1, z0, w)
_sub_in_place(z1, z2, w) # now z1 = a0 b1 + a1 b0
c = [0] * (4 * h + 2) # @line L6: c = z0 + z1 beta^h + z2 beta^(2h)
_add_into(c, z0, 0, w)
_add_into(c, _trim(z1), h, w)
_add_into(c, z2, 2 * h, w)
return c[: 2 * n]
def karatsuba_multiply(a: list[int], b: list[int], w: int = 8, threshold: int = 4) -> list[int]:
"""Product of limb arrays of any lengths m and n, as m + n limbs.
If the lengths are close, the shorter is padded with zeros. If one is less
than half the other, the longer is cut into blocks of the shorter length,
and each block is multiplied by Karatsuba (the unbalanced case).
"""
m, n = len(a), len(b)
if m == 0 or n == 0:
return [0] * (m + n)
if m < n:
a, b, m, n = b, a, n, m
c = [0] * (m + n + 1)
if 2 * n <= m:
for start in range(0, m, n):
block = a[start:start + n]
block = block + [0] * (n - len(block))
_add_into(c, _trim(karatsuba(block, b, w, threshold)), start, w)
else:
p = karatsuba(a, b + [0] * (m - n), w, threshold)
_add_into(c, _trim(p), 0, w)
return c[: m + n]
def karatsuba_square(a: list[int], w: int = 8, threshold: int = 4) -> list[int]:
"""a^2 with three half-size squarings: (a0 + a1)^2 - a0^2 - a1^2 = 2 a0 a1."""
n = len(a)
if n <= max(threshold, 1):
return schoolbook(a, a, w)
h = (n + 1) // 2
a0, a1 = a[:h], a[h:] + [0] * (2 * h - n)
z0 = karatsuba_square(a0, w, threshold)
z2 = karatsuba_square(a1, w, threshold)
z1 = _sum_product(a0, a1, a0, a1, w, threshold, None, lambda x, y, w_, t_, c_: karatsuba_square(x, w_, t_))
_sub_in_place(z1, z0, w)
_sub_in_place(z1, z2, w)
c = [0] * (4 * h + 2)
_add_into(c, z0, 0, w)
_add_into(c, _trim(z1), h, w)
_add_into(c, z2, 2 * h, w)
return c[: 2 * n]
def multiply_signed(x: int, y: int, w: int = 8, threshold: int = 4) -> int:
"""Signed product of Python ints through limb arrays: sign rule and magnitudes."""
def limbs(v: int) -> list[int]:
out = []
while v:
out.append(v & ((1 << w) - 1))
v >>= w
return out
c = karatsuba_multiply(limbs(abs(x)), limbs(abs(y)), w, threshold)
v = 0
for d in reversed(c):
v = (v << w) | d
return -v if (x < 0) != (y < 0) else v
test_karatsuba_multiplication.py 88 lines
# SPDX-License-Identifier: MIT
# Part of https://algorithm.tamnd.com/karatsuba-multiplication/. The authors are listed in prov/QWY1MV.jsonl.
import random
import pytest
from karatsuba_multiplication import karatsuba, karatsuba_multiply, karatsuba_square, multiply_signed, schoolbook
def limbs(x: int, w: int, n: int | None = None) -> list[int]:
out = []
while x:
out.append(x & ((1 << w) - 1))
x >>= w
if n is not None:
out += [0] * (n - len(out))
return out
def value(c: list[int], w: int) -> int:
v = 0
for d in reversed(c):
v = (v << w) | d
return v
def test_example_base_16() -> None:
# 4093 = [13, 15, 15, 0] and 199 = [7, 12, 0, 0] in base 16, padded to 4 limbs
c = karatsuba(limbs(4093, 4, 4), limbs(199, 4, 4), 4, 1)
assert len(c) == 8 and value(c, 4) == 814507
def test_product_count() -> None:
for j in range(8):
counter = [0]
n = 2**j
karatsuba([15] * n, [15] * n, 4, 1, counter)
assert counter[0] == 3**j
def test_edges() -> None:
for w in (4, 8):
top = (1 << w) - 1
for n in (1, 2, 3, 4, 5, 7, 8, 16, 17, 33):
ones = [top] * n
for t in (1, 2, 3):
assert value(karatsuba(ones, ones, w, t), w) == (2 ** (w * n) - 1) ** 2
assert value(karatsuba([0] * n, ones, w, t), w) == 0
one = [1] + [0] * (n - 1)
assert value(karatsuba(one, ones, w, t), w) == 2 ** (w * n) - 1
assert value(karatsuba_square(ones, w, t), w) == (2 ** (w * n) - 1) ** 2
assert karatsuba_multiply([], [1, 2], w) == [0, 0]
assert value(karatsuba_multiply([top], [top], w), w) == top * top
# a carry into a new limb, and very different lengths
assert value(karatsuba_multiply([top] * 40, [2], w, 2), w) == (2 ** (40 * w) - 1) * 2
assert value(karatsuba_multiply([5], [top] * 37, w, 2), w) == 5 * (2 ** (37 * w) - 1)
with pytest.raises(ValueError):
karatsuba([1, 2], [1], 8)
def test_random() -> None:
rng = random.Random(1962)
for w in (4, 8):
for _ in range(450):
m, n = rng.randint(0, 48), rng.randint(0, 48)
if rng.random() < 0.2:
n = rng.randint(1, 4)
x, y = rng.getrandbits(m * w), rng.getrandbits(n * w)
if rng.random() < 0.15:
x = 2 ** (m * w) - 1
a, b = limbs(x, w, m), limbs(y, w, n)
t = rng.choice((1, 2, 4, 8))
c = karatsuba_multiply(a, b, w, t)
assert len(c) == m + n and value(c, w) == x * y
if m == n:
assert karatsuba(a, b, w, t) == schoolbook(a, b, w)
assert value(karatsuba_square(a, w, t), w) == x * x
def test_signed() -> None:
rng = random.Random(7)
for x, y in [(0, -5), (-1, -1), (-255, 256), (2**70, -(2**70) + 1)]:
assert multiply_signed(x, y, 8, 1) == x * y
for _ in range(500):
x = rng.randint(-(2**300), 2**300)
y = rng.randint(-(2**200), 2**200)
assert multiply_signed(x, y, 4, 2) == x * y
What the next level needs
- the tests did not run in this build