Knuth 4.3.1, Algorithm M.
r = r (op) a * b r MUST NOT alias any of a or b.
The result is computed modulo r.len. When r.len >= a.len + b.len, no overflow occurs.
fn llmulaccKaratsuba(
comptime op: AccOp,
allocator: Allocator,
r: []Limb,
a: []const Limb,
b: []const Limb,
) error
fn llmulaccKaratsuba(
comptime op: AccOp,
allocator: Allocator,
r: []Limb,
a: []const Limb,
b: []const Limb,
) error{OutOfMemory}!void {
assert(r.len >= a.len);
assert(a.len >= b.len);
assert(!slicesOverlap(r, a));
assert(!slicesOverlap(r, b));
// Classical karatsuba algorithm:
// a = a1 * B + a0
// b = b1 * B + b0
// Where a0, b0 < B
//
// We then have:
// ab = a * b
// = (a1 * B + a0) * (b1 * B + b0)
// = a1 * b1 * B * B + a1 * B * b0 + a0 * b1 * B + a0 * b0
// = a1 * b1 * B * B + (a1 * b0 + a0 * b1) * B + a0 * b0
//
// Note that:
// a1 * b0 + a0 * b1
// = (a1 + a0)(b1 + b0) - a1 * b1 - a0 * b0
// = (a0 - a1)(b1 - b0) + a1 * b1 + a0 * b0
//
// This yields:
// ab = p2 * B^2 + (p0 + p1 + p2) * B + p0
//
// Where:
// p0 = a0 * b0
// p1 = (a0 - a1)(b1 - b0)
// p2 = a1 * b1
//
// Note, (a0 - a1) and (b1 - b0) produce values -B < x < B, and so we need to mind the sign here.
// We also have:
// 0 <= p0 <= 2B
// -2B <= p1 <= 2B
//
// Note, when B is a multiple of the limb size, multiplies by B amount to shifts or
// slices of a limbs array.
//
// This function computes the result of the multiplication modulo r.len. This means:
// - p2 and p1 only need to be computed modulo r.len - B.
// - In the case of p2, p2 * B^2 needs to be added modulo r.len - 2 * B.
const split = b.len / 2; // B
const limbs_after_split = r.len - split; // Limbs to compute for p1 and p2.
const limbs_after_split2 = r.len - split * 2; // Limbs to add for p2 * B^2.
// For a0 and b0 we need the full range.
const a0 = a[0..llnormalize(a[0..split])];
const b0 = b[0..llnormalize(b[0..split])];
// For a1 and b1 we only need `limbs_after_split` limbs.
const a1 = blk: {
var a1 = a[split..];
a1.len = @min(llnormalize(a1), limbs_after_split);
break :blk a1;
};
const b1 = blk: {
var b1 = b[split..];
b1.len = @min(llnormalize(b1), limbs_after_split);
break :blk b1;
};
// Note that the above slices relative to `split` work because we have a.len > b.len.
// We need some temporary memory to store intermediate results.
// Note, we can reduce the amount of temporaries we need by reordering the computation here:
// ab = p2 * B^2 + (p0 + p1 + p2) * B + p0
// = p2 * B^2 + (p0 * B + p1 * B + p2 * B) + p0
// = (p2 * B^2 + p2 * B) + (p0 * B + p0) + p1 * B
// Allocate at least enough memory to be able to multiply the upper two segments of a and b, assuming
// no overflow.
const tmp = try allocator.alloc(Limb, a.len - split + b.len - split);
defer allocator.free(tmp);
// Compute p2.
// Note, we don't need to compute all of p2, just enough limbs to satisfy r.
const p2_limbs = @min(limbs_after_split, a1.len + b1.len);
@memset(tmp[0..p2_limbs], 0);
llmulacc(.add, allocator, tmp[0..p2_limbs], a1[0..@min(a1.len, p2_limbs)], b1[0..@min(b1.len, p2_limbs)]);
const p2 = tmp[0..llnormalize(tmp[0..p2_limbs])];
// Add p2 * B to the result.
llaccum(op, r[split..], p2);
// Add p2 * B^2 to the result if required.
if (limbs_after_split2 > 0) {
llaccum(op, r[split * 2 ..], p2[0..@min(p2.len, limbs_after_split2)]);
}
// Compute p0.
// Since a0.len, b0.len <= split and r.len >= split * 2, the full width of p0 needs to be computed.
const p0_limbs = a0.len + b0.len;
@memset(tmp[0..p0_limbs], 0);
llmulacc(.add, allocator, tmp[0..p0_limbs], a0, b0);
const p0 = tmp[0..llnormalize(tmp[0..p0_limbs])];
// Add p0 to the result.
llaccum(op, r, p0);
// Add p0 * B to the result. In this case, we may not need all of it.
llaccum(op, r[split..], p0[0..@min(limbs_after_split, p0.len)]);
// Finally, compute and add p1.
// From now on we only need `limbs_after_split` limbs for a0 and b0, since the result of the
// following computation will be added * B.
const a0x = a0[0..@min(a0.len, limbs_after_split)];
const b0x = b0[0..@min(b0.len, limbs_after_split)];
const j0_sign = llcmp(a0x, a1);
const j1_sign = llcmp(b1, b0x);
if (j0_sign * j1_sign == 0) {
// p1 is zero, we don't need to do any computation at all.
return;
}
@memset(tmp, 0);
// p1 is nonzero, so compute the intermediary terms j0 = a0 - a1 and j1 = b1 - b0.
// Note that in this case, we again need some storage for intermediary results
// j0 and j1. Since we have tmp.len >= 2B, we can store both
// intermediaries in the already allocated array.
const j0 = tmp[0 .. a.len - split];
const j1 = tmp[a.len - split ..];
// Ensure that no subtraction overflows.
if (j0_sign == 1) {
// a0 > a1.
_ = llsubcarry(j0, a0x, a1);
} else {
// a0 < a1.
_ = llsubcarry(j0, a1, a0x);
}
if (j1_sign == 1) {
// b1 > b0.
_ = llsubcarry(j1, b1, b0x);
} else {
// b1 > b0.
_ = llsubcarry(j1, b0x, b1);
}
if (j0_sign * j1_sign == 1) {
// If j0 and j1 are both positive, we now have:
// p1 = j0 * j1
// If j0 and j1 are both negative, we now have:
// p1 = -j0 * -j1 = j0 * j1
// In this case we can add p1 to the result using llmulacc.
llmulacc(op, allocator, r[split..], j0[0..llnormalize(j0)], j1[0..llnormalize(j1)]);
} else {
// In this case either j0 or j1 is negative, an we have:
// p1 = -(j0 * j1)
// Now we need to subtract instead of accumulate.
const inverted_op = if (op == .add) .sub else .add;
llmulacc(inverted_op, allocator, r[split..], j0[0..llnormalize(j0)], j1[0..llnormalize(j1)]);
}
}