From 2292395ef226b96da3330c217ef704583318c734 Mon Sep 17 00:00:00 2001 From: mizchi Date: Sat, 23 May 2026 18:20:41 +0900 Subject: [PATCH 1/2] perf(bigint): specialize (n-limb) x (1-limb) multiplication Add a mul_single_limb fast path that runs a single carry-propagating loop in O(self.len). The dispatch in Mul::mul checks both operands so the fast path fires regardless of operand order. For factorial(800)-style chains where one operand is always 1 limb: wasm : 255.1 -> 80.2 ms (-68.6%) wasm-gc : 93.5 -> 25.2 ms (-73.0%) native : 48.9 -> 20.0 ms (-59.1%) The n*n path (Karatsuba) is untouched; bigint_square is within noise. --- bigint/bigint_nonjs.mbt | 32 +++++++++++++++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/bigint/bigint_nonjs.mbt b/bigint/bigint_nonjs.mbt index a3773847e..27f515ad7 100644 --- a/bigint/bigint_nonjs.mbt +++ b/bigint/bigint_nonjs.mbt @@ -384,7 +384,15 @@ pub impl Mul for BigInt with fn mul(self : BigInt, other : BigInt) -> BigInt { if self.is_zero() || other.is_zero() { return zero } - let ret = if self.len < karatsuba_threshold || other.len < karatsuba_threshold { + // Specialize the (n-limb) x (1-limb) case. Factorial-style chains hit + // this on every multiplication, and the general grade-school loop has + // a per-i branch on `j < other_len` plus carry propagation overhead + // that disappears here. + let ret = if other.len == 1 { + self.mul_single_limb(other.limbs[0]) + } else if self.len == 1 { + other.mul_single_limb(self.limbs[0]) + } else if self.len < karatsuba_threshold || other.len < karatsuba_threshold { self.grade_school_mul(other) } else { self.karatsuba_mul(other) @@ -392,6 +400,28 @@ pub impl Mul for BigInt with fn mul(self : BigInt, other : BigInt) -> BigInt { { ..ret, sign: if self.sign == other.sign { Positive } else { Negative } } } +///| +/// Multiply by a single radix-limb. Used as the fast path of `Mul::mul` +/// when one operand has `len == 1`. +fn BigInt::mul_single_limb(self : BigInt, x : UInt) -> BigInt { + let n = self.len + let limbs = FixedArray::make(n + 1, 0U) + let xq = x.to_uint64() + let mut carry = 0UL + for i in 0..> radix_bit_len + } + let len = if carry == 0UL { + n + } else { + limbs[n] = carry.to_uint() + n + 1 + } + { limbs, sign: Positive, len } +} + // Simplest way to multiply two BigInts. ///| From ada88f838098c1dba8c94dd54b609fda14f36e94 Mon Sep 17 00:00:00 2001 From: mizchi Date: Sat, 23 May 2026 18:47:10 +0900 Subject: [PATCH 2/2] docs(bigint): document magnitude-only contract on mul_single_limb Address review comment: mul_single_limb always returns sign: Positive, which is the same convention as grade_school_mul and karatsuba_mul -- Mul::mul overwrites sign with the combined sign of the operands. Spell out that contract in the doc so the helper is not mistaken for a general signed scalar multiply. --- bigint/bigint_nonjs.mbt | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/bigint/bigint_nonjs.mbt b/bigint/bigint_nonjs.mbt index 27f515ad7..f9c91c827 100644 --- a/bigint/bigint_nonjs.mbt +++ b/bigint/bigint_nonjs.mbt @@ -401,8 +401,12 @@ pub impl Mul for BigInt with fn mul(self : BigInt, other : BigInt) -> BigInt { } ///| -/// Multiply by a single radix-limb. Used as the fast path of `Mul::mul` -/// when one operand has `len == 1`. +/// Multiply the magnitude of `self` by a single radix-limb `x`. Returns +/// a `Positive`-signed result regardless of `self.sign` — the caller in +/// `Mul::mul` overwrites `sign` with the correct combined sign. This +/// matches the convention of the other magnitude-only multiply helpers +/// (`grade_school_mul`, `karatsuba_mul`). Do not call directly when a +/// signed product is needed. fn BigInt::mul_single_limb(self : BigInt, x : UInt) -> BigInt { let n = self.len let limbs = FixedArray::make(n + 1, 0U)