Skip to content

Commit 411ef5c

Browse files
JSorngardJASory
andauthored
Machine prime integration (#86)
* Added Machine-Prime, simplified nearest_prime Added the low-memory variant of Machine-prime (using a modified BPSW test) as an optional dependency. The Criterion benchmark shows that it is approximately 16x faster than the existing version. Simplified the next and previous_prime functions * cargo fmt * Rename feature to `fast_test` * Add the `fast_test` feature to docs * Remove commented code * Correct placement of whitespace * Use a typed stride to ensure that `stride` is only ever 1 or `u64::MAX` * Sentences start with capital letters * Add changes to log --------- Co-authored-by: JASory <[email protected]>
1 parent b27f313 commit 411ef5c

File tree

8 files changed

+124
-87
lines changed

8 files changed

+124
-87
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,11 @@
22

33
This file contains the changes to the crate since version 0.4.8.
44

5+
## 0.9.7
6+
7+
- Added the `fast_test` feature that makes `is_prime` call out to the [`machine-prime`](https://crates.io/crates/machine_prime)
8+
crate for a significant speedup.
9+
510
## 0.9.6
611

712
- Correct function name in README.

Cargo.lock

Lines changed: 8 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
[package]
22
name = "const-primes"
33
authors = ["Johanna Sörngård <[email protected]>"]
4-
version = "0.9.6"
4+
version = "0.9.7"
55
edition = "2021"
66
license = "MIT OR Apache-2.0"
77
keywords = ["const", "primes", "no_std", "prime-numbers"]
@@ -16,6 +16,8 @@ serde = { version = "1.0", default-features = false, features = ["derive"], opti
1616
serde_arrays = { version = "0.1.0", optional = true }
1717
zerocopy = { version = "0.8", default-features = false, features = ["derive"], optional = true }
1818
rkyv = { version = "0.8", default-features = false, optional = true }
19+
# no-std constant time primality testing, low memory variant
20+
machine-prime = { version="1.3.*", features = ["small"], optional = true}
1921

2022
[dev-dependencies]
2123
criterion = { version = "0.5", features = ["html_reports"] }
@@ -33,6 +35,10 @@ zerocopy = ["dep:zerocopy"]
3335
# Derives the `Serialize`, `Deserialize`, and `Archive` traits from the [`rkyv`](https://crates.io/crates/rkyv) crate for the `Primes` struct.
3436
rkyv = ["dep:rkyv"]
3537

38+
# Speeds up primality testing significantly by using the [`machine-prime`](https://crates.io/crates/machine-prime) crate.
39+
fast_test = ["dep:machine-prime"]
40+
41+
3642
[package.metadata.docs.rs]
3743
# Document all features.
3844
all-features = true

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,9 @@ for the `Primes` struct.
114114
`rkyv`: derives the `Serialize`, `Deserialize`, and `Archive` traits from
115115
[`rkyv`](https://crates.io/crates/rkyv) for the `Primes` struct.
116116

117+
`fast_test`: speeds up primality testing significantly by using the
118+
[`machine-prime`](https://crates.io/crates/machine-prime) crate.
119+
117120
<br>
118121

119122
### License

src/check.rs

Lines changed: 59 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
//! This module contains an implementation of a deterministic Miller-Rabin primality test
2-
2+
#[cfg(not(feature = "fast_test"))]
33
use crate::integer_math::{mod_mul, mod_pow};
44

55
/// Returns whether `n` is prime.
@@ -17,67 +17,76 @@ use crate::integer_math::{mod_mul, mod_pow};
1717
/// ```
1818
#[must_use]
1919
pub const fn is_prime(n: u64) -> bool {
20-
// Since we know the maximum size of the numbers we test against
21-
// we can use the fact that there are known perfect bases
22-
// in order to make the test both fast and deterministic.
23-
// This list of witnesses was taken from
24-
// <https://en.wikipedia.org/wiki/Miller%E2%80%93Rabin_primality_test#Testing_against_small_sets_of_bases>.
25-
const NUM_BASES: usize = 11;
26-
const WITNESSES: [(u64, &[u64]); NUM_BASES] = [
27-
(2_046, &[2]),
28-
(1_373_652, &[2, 3]),
29-
(9_080_190, &[31, 73]),
30-
(25_326_000, &[2, 3, 5]),
31-
(4_759_123_140, &[2, 7, 61]),
32-
(1_112_004_669_632, &[2, 13, 23, 1_662_803]),
33-
(2_152_302_898_746, &[2, 3, 5, 7, 11]),
34-
(3_474_749_660_382, &[2, 3, 5, 7, 11, 13]),
35-
(341_550_071_728_320, &[2, 3, 5, 7, 11, 13, 17]),
36-
(3_825_123_056_546_413_050, &[2, 3, 5, 7, 11, 13, 17, 19, 23]),
37-
(u64::MAX, &[2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37]),
38-
];
39-
40-
if n == 2 || n == 3 {
41-
return true;
42-
} else if n <= 1 || n % 2 == 0 || n % 3 == 0 {
43-
return false;
20+
#[cfg(feature = "fast_test")]
21+
{
22+
machine_prime::is_prime(n)
4423
}
4524

46-
// Use a small wheel to check up to log2(n).
47-
// This keeps the complexity at O(log(n)).
48-
let mut candidate_factor = 5;
49-
let trial_limit = n.ilog2() as u64;
50-
while candidate_factor <= trial_limit {
51-
if n % candidate_factor == 0 || n % (candidate_factor + 2) == 0 {
25+
#[cfg(not(feature = "fast_test"))]
26+
{
27+
// Since we know the maximum size of the numbers we test against
28+
// we can use the fact that there are known perfect bases
29+
// in order to make the test both fast and deterministic.
30+
// This list of witnesses was taken from
31+
// <https://en.wikipedia.org/wiki/Miller%E2%80%93Rabin_primality_test#Testing_against_small_sets_of_bases>.
32+
const NUM_BASES: usize = 11;
33+
const WITNESSES: [(u64, &[u64]); NUM_BASES] = [
34+
(2_046, &[2]),
35+
(1_373_652, &[2, 3]),
36+
(9_080_190, &[31, 73]),
37+
(25_326_000, &[2, 3, 5]),
38+
(4_759_123_140, &[2, 7, 61]),
39+
(1_112_004_669_632, &[2, 13, 23, 1_662_803]),
40+
(2_152_302_898_746, &[2, 3, 5, 7, 11]),
41+
(3_474_749_660_382, &[2, 3, 5, 7, 11, 13]),
42+
(341_550_071_728_320, &[2, 3, 5, 7, 11, 13, 17]),
43+
(3_825_123_056_546_413_050, &[2, 3, 5, 7, 11, 13, 17, 19, 23]),
44+
(u64::MAX, &[2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37]),
45+
];
46+
47+
if n == 2 || n == 3 {
48+
return true;
49+
} else if n <= 1 || n % 2 == 0 || n % 3 == 0 {
5250
return false;
5351
}
54-
candidate_factor += 6;
55-
}
5652

57-
// Find r such that n = 2^d * r + 1 for some r >= 1
58-
let mut d = n - 1;
59-
while d % 2 == 0 {
60-
d >>= 1;
61-
}
53+
// Use a small wheel to check up to log2(n).
54+
// This keeps the complexity at O(log(n)).
55+
let mut candidate_factor = 5;
56+
let trial_limit = n.ilog2() as u64;
57+
while candidate_factor <= trial_limit {
58+
if n % candidate_factor == 0 || n % (candidate_factor + 2) == 0 {
59+
return false;
60+
}
61+
candidate_factor += 6;
62+
}
6263

63-
let mut i = 0;
64-
while i < NUM_BASES && WITNESSES[i].0 < n {
65-
i += 1;
66-
}
67-
let witnesses = WITNESSES[i].1;
64+
// Find r such that n = 2^d * r + 1 for some r >= 1
65+
let mut d = n - 1;
66+
while d % 2 == 0 {
67+
d >>= 1;
68+
}
6869

69-
let mut i = 0;
70-
while i < witnesses.len() && witnesses[i] < n {
71-
if !miller_test(d, n, witnesses[i]) {
72-
return false;
70+
let mut i = 0;
71+
while i < NUM_BASES && WITNESSES[i].0 < n {
72+
i += 1;
73+
}
74+
let witnesses = WITNESSES[i].1;
75+
76+
let mut i = 0;
77+
while i < witnesses.len() && witnesses[i] < n {
78+
if !miller_test(d, n, witnesses[i]) {
79+
return false;
80+
}
81+
i += 1;
7382
}
74-
i += 1;
75-
}
7683

77-
true
84+
true
85+
} // end conditional compilation block
7886
}
7987

8088
/// Performs a Miller-Rabin test with the witness k.
89+
#[cfg(not(feature = "fast_test"))]
8190
const fn miller_test(mut d: u64, n: u64, k: u64) -> bool {
8291
let mut x = mod_pow(k, d, n);
8392
if x == 1 || x == n - 1 {

src/integer_math.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ pub const fn isqrt(n: u64) -> u64 {
3434

3535
/// Calculates (`base` ^ `exp`) mod `modulo` without overflow.
3636
#[must_use]
37+
#[cfg(not(feature = "fast_test"))]
3738
pub const fn mod_pow(mut base: u64, mut exp: u64, modulo: u64) -> u64 {
3839
let mut res = 1;
3940

@@ -52,6 +53,7 @@ pub const fn mod_pow(mut base: u64, mut exp: u64, modulo: u64) -> u64 {
5253

5354
/// Calculates (`a` * `b`) mod `modulo` without overflow.
5455
#[must_use]
56+
#[cfg(not(feature = "fast_test"))]
5557
pub const fn mod_mul(a: u64, b: u64, modulo: u64) -> u64 {
5658
((a as u128 * b as u128) % modulo as u128) as u64
5759
}

src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,8 @@
9797
//! `zerocopy`: derives the [`IntoBytes`](zerocopy::IntoBytes) trait from the [`zerocopy`] crate for the [`Primes`] struct.
9898
//!
9999
//! `rkyv`: derives the [`Serialize`](rkyv::Serialize), [`Deserialize`](rkyv::Deserialize), and [`Archive`](rkyv::Archive) traits from the [`rkyv`] crate for the [`Primes`] struct.
100+
//!
101+
//! `fast_test`: speeds up primality testing significantly by using the [`machine-prime`](https://crates.io/crates/machine-prime) crate.
100102
101103
#![forbid(unsafe_code)]
102104
#![no_std]

src/search.rs

Lines changed: 38 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,27 @@
22
33
use crate::is_prime;
44

5+
/// Generalised function for nearest search by incrementing/decrementing by 1
6+
/// Any attempt at optimising this would be largely pointless since the largest prime gap under 2^64 is only 1550
7+
/// And is_prime's trial division already eliminates most of those
8+
const fn bounded_search(mut n: u64, stride: Stride) -> Option<u64> {
9+
let stride = stride.into_u64();
10+
11+
loop {
12+
// Addition over Z/2^64, aka regular addition under optimisation flags
13+
n = n.wrapping_add(stride);
14+
15+
// If either condition is met then we started either below or above the smallest or largest prime respectively
16+
// Any two values from 2^64-58 to 1 would also work
17+
if n == 0u64 || n == u64::MAX {
18+
return None;
19+
}
20+
21+
if is_prime(n) {
22+
return Some(n);
23+
}
24+
}
25+
}
526
/// Returns the largest prime smaller than `n` if there is one.
627
///
728
/// Scans for primes downwards from the input with [`is_prime`].
@@ -17,31 +38,15 @@ use crate::is_prime;
1738
/// ```
1839
///
1940
/// There's no prime smaller than two:
20-
/// ```
2141
///
42+
/// ```
2243
/// # use const_primes::previous_prime;
2344
/// const NO_SUCH: Option<u64> = previous_prime(2);
2445
/// assert_eq!(NO_SUCH, None);
2546
/// ```
2647
#[must_use = "the function only returns a new value and does not modify its input"]
27-
pub const fn previous_prime(mut n: u64) -> Option<u64> {
28-
if n <= 2 {
29-
None
30-
} else if n == 3 {
31-
Some(2)
32-
} else {
33-
n -= 1;
34-
35-
if n % 2 == 0 {
36-
n -= 1;
37-
}
38-
39-
while !is_prime(n) {
40-
n -= 2;
41-
}
42-
43-
Some(n)
44-
}
48+
pub const fn previous_prime(n: u64) -> Option<u64> {
49+
bounded_search(n, Stride::Down)
4550
}
4651

4752
/// Returns the smallest prime greater than `n` if there is one that
@@ -60,30 +65,28 @@ pub const fn previous_prime(mut n: u64) -> Option<u64> {
6065
/// ```
6166
///
6267
/// Primes larger than 18446744073709551557 can not be represented by a `u64`:
63-
/// ```
6468
///
69+
/// ```
6570
/// # use const_primes::next_prime;
6671
/// const NO_SUCH: Option<u64> = next_prime(18_446_744_073_709_551_557);
6772
/// assert_eq!(NO_SUCH, None);
6873
/// ```
6974
#[must_use = "the function only returns a new value and does not modify its input"]
70-
pub const fn next_prime(mut n: u64) -> Option<u64> {
71-
// The largest prime smaller than u64::MAX
72-
if n >= 18_446_744_073_709_551_557 {
73-
None
74-
} else if n <= 1 {
75-
Some(2)
76-
} else {
77-
n += 1;
75+
pub const fn next_prime(n: u64) -> Option<u64> {
76+
bounded_search(n, Stride::Up)
77+
}
7878

79-
if n % 2 == 0 {
80-
n += 1;
81-
}
79+
enum Stride {
80+
Up,
81+
Down,
82+
}
8283

83-
while !is_prime(n) {
84-
n += 2;
84+
impl Stride {
85+
const fn into_u64(self) -> u64 {
86+
match self {
87+
Self::Up => 1,
88+
// Adding by 2^64-1 over Z/2^64 is equivalent to subtracting by 1
89+
Self::Down => u64::MAX,
8590
}
86-
87-
Some(n)
8891
}
8992
}

0 commit comments

Comments
 (0)