From 91a1f0d918f54e3b3425248288085db4d5619075 Mon Sep 17 00:00:00 2001 From: Riccardo Gerosa Date: Mon, 24 Oct 2022 22:41:02 +0000 Subject: [PATCH] math/big: improve performance of Binomial MIME-Version: 1.0 Content-Type: text/plain; charset=utf8 Content-Transfer-Encoding: 8bit This change improves the performance of Binomial by implementing an algorithm that produces smaller intermediate values at each step. Working with smaller big.Int values has the advantage that fewer allocations and computations are required for each mathematical operation. The algorithm used is the Multiplicative Formula, which is a well known way of calculating the Binomial coefficient and is described at: https://en.wikipedia.org/wiki/Binomial_coefficient#Multiplicative_formula https://en.wikipedia.org/wiki/Binomial_coefficient#In_programming_languages In addition to that, an optimization has been made to remove a redundant computation of (i+1) on each loop which has a measurable impact when using big.Int. Performance improvement measured on an M1 MacBook Pro running the existing benchmark for Binomial: name old time/op new time/op delta Binomial-8 589ns ± 0% 435ns ± 0% -26.05% (p=0.000 n=10+10) name old alloc/op new alloc/op delta Binomial-8 1.02kB ± 0% 0.08kB ± 0% -92.19% (p=0.000 n=10+10) name old allocs/op new allocs/op delta Binomial-8 38.0 ± 0% 5.0 ± 0% -86.84% (p=0.000 n=10+10) Change-Id: I5a830386dd42f062e17af88411dd74fcb110ded9 GitHub-Last-Rev: 6b2fca07de4096accb02f66c313dff47c2303462 GitHub-Pull-Request: golang/go#56339 Reviewed-on: https://go-review.googlesource.com/c/go/+/444315 Auto-Submit: Robert Griesemer Reviewed-by: Dmitri Shuralyov Reviewed-by: Robert Griesemer Run-TryBot: Robert Griesemer --- src/math/big/int.go | 46 +++++++++++++++++++++++++++++++++++++-------- 1 file changed, 38 insertions(+), 8 deletions(-) diff --git a/src/math/big/int.go b/src/math/big/int.go index ca4c3561e6..a26fdbb90e 100644 --- a/src/math/big/int.go +++ b/src/math/big/int.go @@ -205,16 +205,46 @@ func (z *Int) MulRange(a, b int64) *Int { return z } -// Binomial sets z to the binomial coefficient of (n, k) and returns z. -func (z *Int) Binomial(n, k int64) *Int { +// Binomial sets z to the binomial coefficient C(n, k) and returns z. +func (z *Int) Binomial(n_, k_ int64) *Int { + if k_ > n_ { + return z.SetInt64(0) + } // reduce the number of multiplications by reducing k - if n/2 < k && k <= n { - k = n - k // Binomial(n, k) == Binomial(n, n-k) + if k_ > n_-k_ { + k_ = n_ - k_ // C(n, k) == C(n, n-k) + } + // C(n, k) == n * (n-1) * ... * (n-k+1) / k * (k-1) * ... * 1 + // == n * (n-1) * ... * (n-k+1) / 1 * (1+1) * ... * k + // + // Using the multiplicative formula produces smaller values + // at each step, requiring fewer allocations and computations: + // + // z = 1 + // for i := 0; i < k; i = i+1 { + // z *= n-i + // z /= i+1 + // } + // + // finally to avoid computing i+1 twice per loop: + // + // z = 1 + // i := 0 + // for i < k { + // z *= n-i + // i++ + // z /= i + // } + var n, k, i, t Int + n.SetInt64(n_) + k.SetInt64(k_) + z.Set(intOne) + for i.Cmp(&k) < 0 { + z.Mul(z, t.Sub(&n, &i)) + i.Add(&i, intOne) + z.Quo(z, &i) } - var a, b Int - a.MulRange(n-k+1, n) - b.MulRange(1, k) - return z.Quo(&a, &b) + return z } // Quo sets z to the quotient x/y for y != 0 and returns z. -- 2.50.0