Skip to content

Commit 07a2d6a

Browse files
committed
Merge #15
15: Implement Stein's algorithm for gcd r=cuviper a=Emerentius This implements Stein's algorithm for bigints. Asymptotically this has the same runtime complexity as the euclidean algorithm but it's faster because it avoids division in favor of bitshifts and subtractions. There are faster algorithms for large bigints. For small ones, [gmp uses the binary gcd too](https://gmplib.org/manual/Binary-GCD.html). I've run some benchmarks with the code in [this repo](https://github.com/Emerentius/bigint_gcd_bench) This iterates through the sizes of 1-10 `BigDigit`s and generates 300 uniformly distributed random bigints at each size and computes the gcd for each combination with both Euclid's and Stein's algorithm. I'm only looking at combinations of numbers with the same number of `BigDigit`s The speed gains are sizeable. See the benchmark results below. I'm running this on an ultrabook with a 15W CPU (i5 4210u). Performance may differ on different architectures, in particular if there is no intrinsic for counting trailing zeroes. Please run the benchmark on your machine. It's just a simple ``` git clone https://github.com/Emerentius/bigint_gcd_bench cargo run --release ``` ``` 2^32n bits euclidean gcd binary gcd speedup n: 1 => 0.3050s 0.0728s 4.19 n: 2 => 0.6228s 0.1453s 4.29 n: 3 => 0.9618s 0.2214s 4.34 n: 4 => 1.3021s 0.3028s 4.30 n: 5 => 1.6469s 0.3875s 4.25 n: 6 => 2.0017s 0.4759s 4.21 n: 7 => 2.3636s 0.5667s 4.17 n: 8 => 2.7284s 0.6418s 4.25 n: 9 => 3.0712s 0.7302s 4.21 n: 10 => 3.4822s 0.8223s 4.23 ``` The guys at gmp say these algorithms are quadratic in N, I'm not sure why they seem almost linear here.
2 parents c0ab6c3 + e45b2b7 commit 07a2d6a

File tree

6 files changed

+135
-12
lines changed

6 files changed

+135
-12
lines changed

Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@ readme = "README.md"
1414
[[bench]]
1515
name = "bigint"
1616

17+
[[bench]]
18+
name = "gcd"
19+
1720
[[bench]]
1821
harness = false
1922
name = "shootout-pidigits"

benches/gcd.rs

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
#![feature(test)]
2+
3+
extern crate test;
4+
extern crate num_bigint;
5+
extern crate num_integer;
6+
extern crate num_traits;
7+
extern crate rand;
8+
9+
use test::Bencher;
10+
use num_bigint::{BigUint, RandBigInt};
11+
use num_integer::Integer;
12+
use num_traits::Zero;
13+
use rand::{SeedableRng, StdRng};
14+
15+
fn get_rng() -> StdRng {
16+
let seed: &[_] = &[1, 2, 3, 4];
17+
SeedableRng::from_seed(seed)
18+
}
19+
20+
fn bench(b: &mut Bencher, bits: usize, gcd: fn(&BigUint, &BigUint) -> BigUint) {
21+
let mut rng = get_rng();
22+
let x = rng.gen_biguint(bits);
23+
let y = rng.gen_biguint(bits);
24+
25+
assert_eq!(euclid(&x, &y), x.gcd(&y));
26+
27+
b.iter(|| gcd(&x, &y));
28+
}
29+
30+
31+
fn euclid(x: &BigUint, y: &BigUint) -> BigUint {
32+
// Use Euclid's algorithm
33+
let mut m = x.clone();
34+
let mut n = y.clone();
35+
while !m.is_zero() {
36+
let temp = m;
37+
m = n % &temp;
38+
n = temp;
39+
}
40+
return n;
41+
}
42+
43+
#[bench]
44+
fn gcd_euclid_0064(b: &mut Bencher) {
45+
bench(b, 64, euclid);
46+
}
47+
48+
#[bench]
49+
fn gcd_euclid_0256(b: &mut Bencher) {
50+
bench(b, 256, euclid);
51+
}
52+
53+
#[bench]
54+
fn gcd_euclid_1024(b: &mut Bencher) {
55+
bench(b, 1024, euclid);
56+
}
57+
58+
#[bench]
59+
fn gcd_euclid_4096(b: &mut Bencher) {
60+
bench(b, 4096, euclid);
61+
}
62+
63+
64+
// Integer for BigUint now uses Stein for gcd
65+
66+
#[bench]
67+
fn gcd_stein_0064(b: &mut Bencher) {
68+
bench(b, 64, BigUint::gcd);
69+
}
70+
71+
#[bench]
72+
fn gcd_stein_0256(b: &mut Bencher) {
73+
bench(b, 256, BigUint::gcd);
74+
}
75+
76+
#[bench]
77+
fn gcd_stein_1024(b: &mut Bencher) {
78+
bench(b, 1024, BigUint::gcd);
79+
}
80+
81+
#[bench]
82+
fn gcd_stein_4096(b: &mut Bencher) {
83+
bench(b, 4096, BigUint::gcd);
84+
}

src/algorithms.rs

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -591,9 +591,12 @@ pub fn biguint_shr(n: Cow<BigUint>, bits: usize) -> BigUint {
591591
if n_unit >= n.data.len() {
592592
return Zero::zero();
593593
}
594-
let mut data = match n_unit {
595-
0 => n.into_owned().data,
596-
_ => n.data[n_unit..].to_vec(),
594+
let mut data = match n {
595+
Cow::Borrowed(n) => n.data[n_unit..].to_vec(),
596+
Cow::Owned(mut n) => {
597+
n.data.drain(..n_unit);
598+
n.data
599+
}
597600
};
598601

599602
let n_bits = bits % big_digit::BITS;

src/bigint.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ use std::str::{self, FromStr};
44
use std::fmt;
55
use std::cmp::Ordering::{self, Less, Greater, Equal};
66
use std::{i64, u64};
7+
#[allow(unused)]
78
use std::ascii::AsciiExt;
89

910
#[cfg(feature = "serde")]

src/biguint.rs

Lines changed: 40 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,11 @@ use std::ops::{Add, BitAnd, BitOr, BitXor, Div, Mul, Neg, Rem, Shl, Shr, Sub,
77
use std::str::{self, FromStr};
88
use std::fmt;
99
use std::cmp;
10+
use std::mem;
1011
use std::cmp::Ordering::{self, Less, Greater, Equal};
1112
use std::{f32, f64};
1213
use std::{u8, u64};
14+
#[allow(unused)]
1315
use std::ascii::AsciiExt;
1416

1517
#[cfg(feature = "serde")]
@@ -396,7 +398,8 @@ impl<'a> Shr<usize> for &'a BigUint {
396398
impl ShrAssign<usize> for BigUint {
397399
#[inline]
398400
fn shr_assign(&mut self, rhs: usize) {
399-
*self = biguint_shr(Cow::Borrowed(&*self), rhs);
401+
let n = mem::replace(self, BigUint::zero());
402+
*self = n >> rhs;
400403
}
401404
}
402405

@@ -940,16 +943,34 @@ impl Integer for BigUint {
940943
///
941944
/// The result is always positive.
942945
#[inline]
943-
fn gcd(&self, other: &BigUint) -> BigUint {
944-
// Use Euclid's algorithm
945-
let mut m = (*self).clone();
946-
let mut n = (*other).clone();
946+
fn gcd(&self, other: &Self) -> Self {
947+
// Stein's algorithm
948+
if self.is_zero() {
949+
return other.clone();
950+
}
951+
if other.is_zero() {
952+
return self.clone();
953+
}
954+
let mut m = self.clone();
955+
let mut n = other.clone();
956+
957+
// find common factors of 2
958+
let shift = cmp::min(
959+
n.trailing_zeros(),
960+
m.trailing_zeros()
961+
);
962+
963+
// divide m and n by 2 until odd
964+
// m inside loop
965+
n >>= n.trailing_zeros();
966+
947967
while !m.is_zero() {
948-
let temp = m;
949-
m = n % &temp;
950-
n = temp;
968+
m >>= m.trailing_zeros();
969+
if n > m { mem::swap(&mut n, &mut m) }
970+
m -= &n;
951971
}
952-
return n;
972+
973+
n << shift
953974
}
954975

955976
/// Calculates the Lowest Common Multiple (LCM) of the number and `other`.
@@ -1607,6 +1628,16 @@ impl BigUint {
16071628
return self.data.len() * big_digit::BITS - zeros as usize;
16081629
}
16091630

1631+
// self is assumed to be normalized
1632+
fn trailing_zeros(&self) -> usize {
1633+
self.data
1634+
.iter()
1635+
.enumerate()
1636+
.find(|&(_, &digit)| digit != 0)
1637+
.map(|(i, digit)| i * big_digit::BITS + digit.trailing_zeros() as usize)
1638+
.unwrap_or(0)
1639+
}
1640+
16101641
/// Strips off trailing zero bigdigits - comparisons require the last element in the vector to
16111642
/// be nonzero.
16121643
#[inline]

src/tests/biguint.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1569,6 +1569,7 @@ fn test_from_str_radix() {
15691569

15701570
#[test]
15711571
fn test_all_str_radix() {
1572+
#[allow(unused)]
15721573
use std::ascii::AsciiExt;
15731574

15741575
let n = BigUint::new((0..10).collect());

0 commit comments

Comments
 (0)