Skip to content

Commit 0267b9d

Browse files
committed
Generalize sieving
1 parent d2c4a39 commit 0267b9d

File tree

10 files changed

+325
-126
lines changed

10 files changed

+325
-126
lines changed

.github/workflows/crypto-primes.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ jobs:
4444
- name: Install cargo-llvm-cov
4545
uses: taiki-e/install-action@cargo-llvm-cov
4646
- name: Generate code coverage
47-
run: cargo llvm-cov --workspace --lcov --output-path lcov.info
47+
run: cargo llvm-cov --features default-rng,multicore --workspace --lcov --output-path lcov.info
4848
- name: Upload coverage to Codecov
4949
uses: codecov/codecov-action@v4
5050
with:
@@ -99,7 +99,7 @@ jobs:
9999
profile: minimal
100100
override: true
101101
- run: ${{ matrix.deps }}
102-
- run: cargo test --target ${{ matrix.target }} --release --features tests-all
102+
- run: cargo test --target ${{ matrix.target }} --release --all-features
103103

104104
clippy:
105105
runs-on: ubuntu-latest

CHANGELOG.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
66

77
## Unreleased
88

9+
### Changed
10+
11+
- Renamed `Sieve` to `SmallPrimesSieve`. ([#64])
12+
13+
914
### Added
15+
1016
- Parallel prime finding methods and a "multicore" feature ([#60])
17+
- Generalized sieving: `SieveFactory` trait, `SieveIterator`, and the convenience functions `sieve_and_find()` and `par_sieve_and_find()`. ([#64])
18+
1119

1220
[#60]: https://github.com/entropyxyz/crypto-primes/pull/60
21+
[#64]: https://github.com/entropyxyz/crypto-primes/pull/64
22+
1323

1424
## [0.6.0-pre.2] - 2024-10-18
1525

benches/bench.rs

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,22 @@ use core::num::NonZero;
33
use criterion::{criterion_group, criterion_main, BatchSize, Criterion};
44
use crypto_bigint::{nlimbs, BoxedUint, Integer, Odd, RandomBits, Uint, U1024, U128, U256};
55
use rand_chacha::ChaCha8Rng;
6-
use rand_core::{CryptoRngCore, OsRng, RngCore, SeedableRng};
6+
use rand_core::{CryptoRngCore, OsRng, SeedableRng};
77

88
#[cfg(feature = "tests-gmp")]
99
use rug::{integer::Order, Integer as GmpInteger};
1010

1111
#[cfg(feature = "tests-openssl")]
1212
use openssl::bn::BigNum;
1313

14+
#[cfg(feature = "multicore")]
15+
use rand_core::RngCore;
16+
1417
use crypto_primes::{
1518
generate_prime_with_rng, generate_safe_prime_with_rng,
1619
hazmat::{
17-
lucas_test, random_odd_integer, AStarBase, BruteForceBase, LucasCheck, MillerRabin, SelfridgeBase, Sieve,
20+
lucas_test, random_odd_integer, AStarBase, BruteForceBase, LucasCheck, MillerRabin, SelfridgeBase,
21+
SmallPrimesSieve,
1822
},
1923
is_prime_with_rng, is_safe_prime_with_rng,
2024
};
@@ -25,6 +29,7 @@ fn make_rng() -> ChaCha8Rng {
2529
ChaCha8Rng::from_seed(*b"01234567890123456789012345678901")
2630
}
2731

32+
#[cfg(feature = "multicore")]
2833
fn make_random_rng() -> ChaCha8Rng {
2934
let mut seed = <ChaCha8Rng as SeedableRng>::Seed::default();
3035
OsRng.fill_bytes(&mut seed);
@@ -35,9 +40,9 @@ fn random_odd_uint<T: RandomBits + Integer>(rng: &mut impl CryptoRngCore, bit_le
3540
random_odd_integer::<T>(rng, NonZero::new(bit_length).unwrap())
3641
}
3742

38-
fn make_sieve<const L: usize>(rng: &mut impl CryptoRngCore) -> Sieve<Uint<L>> {
43+
fn make_sieve<const L: usize>(rng: &mut impl CryptoRngCore) -> SmallPrimesSieve<Uint<L>> {
3944
let start = random_odd_uint::<Uint<L>>(rng, Uint::<L>::BITS);
40-
Sieve::new(start.get(), NonZero::new(Uint::<L>::BITS).unwrap(), false)
45+
SmallPrimesSieve::new(start.get(), NonZero::new(Uint::<L>::BITS).unwrap(), false)
4146
}
4247

4348
fn make_presieved_num<const L: usize>(rng: &mut impl CryptoRngCore) -> Odd<Uint<L>> {
@@ -55,7 +60,7 @@ fn bench_sieve(c: &mut Criterion) {
5560
group.bench_function("(U128) creation", |b| {
5661
b.iter_batched(
5762
|| random_odd_uint::<U128>(&mut OsRng, 128),
58-
|start| Sieve::new(start.get(), NonZero::new(128).unwrap(), false),
63+
|start| SmallPrimesSieve::new(start.get(), NonZero::new(128).unwrap(), false),
5964
BatchSize::SmallInput,
6065
)
6166
});
@@ -76,7 +81,7 @@ fn bench_sieve(c: &mut Criterion) {
7681
group.bench_function("(U1024) creation", |b| {
7782
b.iter_batched(
7883
|| random_odd_uint::<U1024>(&mut OsRng, 1024),
79-
|start| Sieve::new(start.get(), NonZero::new(1024).unwrap(), false),
84+
|start| SmallPrimesSieve::new(start.get(), NonZero::new(1024).unwrap(), false),
8085
BatchSize::SmallInput,
8186
)
8287
});
@@ -445,7 +450,7 @@ fn bench_glass_pumpkin(c: &mut Criterion) {
445450
fn prime_like_gp(bit_length: u32, rng: &mut impl CryptoRngCore) -> BoxedUint {
446451
loop {
447452
let start = random_odd_integer::<BoxedUint>(rng, NonZero::new(bit_length).unwrap()).get();
448-
let sieve = Sieve::new(start, NonZero::new(bit_length).unwrap(), false);
453+
let sieve = SmallPrimesSieve::new(start, NonZero::new(bit_length).unwrap(), false);
449454
for num in sieve {
450455
let odd_num = Odd::new(num.clone()).unwrap();
451456

@@ -469,7 +474,7 @@ fn bench_glass_pumpkin(c: &mut Criterion) {
469474
fn safe_prime_like_gp(bit_length: u32, rng: &mut impl CryptoRngCore) -> BoxedUint {
470475
loop {
471476
let start = random_odd_integer::<BoxedUint>(rng, NonZero::new(bit_length).unwrap()).get();
472-
let sieve = Sieve::new(start, NonZero::new(bit_length).unwrap(), true);
477+
let sieve = SmallPrimesSieve::new(start, NonZero::new(bit_length).unwrap(), true);
473478
for num in sieve {
474479
let odd_num = Odd::new(num.clone()).unwrap();
475480

src/generic.rs

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
use rand_core::CryptoRngCore;
2+
3+
#[cfg(feature = "multicore")]
4+
use rayon::iter::{ParallelBridge, ParallelIterator};
5+
6+
use crate::SieveFactory;
7+
8+
/// Sieves through the results of `sieve_factory` and returns the first item for which `predicate` is `true`.
9+
///
10+
/// If `sieve_factory` signals that no more results can be created, returns `None`.
11+
pub fn sieve_and_find<R, S>(
12+
rng: &mut R,
13+
sieve_factory: S,
14+
predicate: impl Fn(&mut R, &S::Item) -> bool,
15+
) -> Option<S::Item>
16+
where
17+
S: SieveFactory,
18+
R: CryptoRngCore,
19+
{
20+
// We could use `SieveIterator` here, but it requires cloning the `rng`.
21+
// Unlike the parallel version, it is avoidable here.
22+
23+
let mut sieve_factory = sieve_factory;
24+
let mut sieve = sieve_factory.make_sieve(rng, None)?;
25+
26+
loop {
27+
if let Some(value) = sieve.find(|num| predicate(rng, num)) {
28+
return Some(value);
29+
}
30+
if let Some(new_sieve) = sieve_factory.make_sieve(rng, Some(&sieve)) {
31+
sieve = new_sieve;
32+
} else {
33+
return None;
34+
}
35+
}
36+
}
37+
38+
/// Sieves through the results of `sieve_factory` using a thread pool with `threadcount` threads,
39+
/// and returns the first item for which `predicate` is `true`.
40+
///
41+
/// If `sieve_factory` signals that no more results can be created, returns `None`.
42+
#[cfg(feature = "multicore")]
43+
pub fn par_sieve_and_find<R, S, F>(rng: &mut R, sieve_factory: S, predicate: F, threadcount: usize) -> Option<S::Item>
44+
where
45+
R: CryptoRngCore + Clone + Send + Sync,
46+
S: Send + Sync + SieveFactory,
47+
S::Sieve: Send,
48+
S::Item: Send,
49+
F: Sync + Fn(&mut R, &S::Item) -> bool,
50+
{
51+
let threadpool = rayon::ThreadPoolBuilder::new()
52+
.num_threads(threadcount)
53+
.build()
54+
.expect("If the platform can spawn threads, then this call will work.");
55+
56+
let mut iter_rng = rng.clone();
57+
let iter = SieveIterator::new(&mut iter_rng, sieve_factory)?;
58+
59+
threadpool.install(|| {
60+
iter.par_bridge().find_any(|c| {
61+
let mut rng = rng.clone();
62+
predicate(&mut rng, c)
63+
})
64+
})
65+
}
66+
67+
/// A structure that chains the creation of sieves, returning the results from one until it is exhausted,
68+
/// and then creating a new one.
69+
#[derive(Debug)]
70+
pub struct SieveIterator<'a, R: CryptoRngCore, S: SieveFactory> {
71+
sieve_factory: S,
72+
sieve: S::Sieve,
73+
rng: &'a mut R,
74+
}
75+
76+
impl<'a, R: CryptoRngCore, S: SieveFactory> SieveIterator<'a, R, S> {
77+
/// Creates a new chained iterator producing results from sieves returned from `sieve_factory`.
78+
pub fn new(rng: &'a mut R, sieve_factory: S) -> Option<Self> {
79+
let mut sieve_factory = sieve_factory;
80+
let sieve = sieve_factory.make_sieve(rng, None)?;
81+
Some(Self {
82+
sieve_factory,
83+
rng,
84+
sieve,
85+
})
86+
}
87+
}
88+
89+
impl<R: CryptoRngCore, S: SieveFactory> Iterator for SieveIterator<'_, R, S> {
90+
type Item = S::Item;
91+
92+
fn next(&mut self) -> Option<Self::Item> {
93+
loop {
94+
if let Some(result) = self.sieve.next() {
95+
return Some(result);
96+
}
97+
98+
self.sieve = self.sieve_factory.make_sieve(self.rng, Some(&self.sieve))?;
99+
}
100+
}
101+
}
102+
103+
#[cfg(test)]
104+
mod tests {
105+
use rand_core::{CryptoRngCore, OsRng};
106+
107+
use super::sieve_and_find;
108+
use crate::SieveFactory;
109+
110+
#[cfg(feature = "multicore")]
111+
use super::par_sieve_and_find;
112+
113+
#[test]
114+
fn test_exhaustable_sieve_factory() {
115+
// Test the logic handling the case of the sieve factory not being able to produce new sieves.
116+
struct TestSieveFactory {
117+
count: usize,
118+
}
119+
120+
impl SieveFactory for TestSieveFactory {
121+
type Item = usize;
122+
type Sieve = core::ops::Range<usize>;
123+
124+
fn make_sieve(
125+
&mut self,
126+
_rng: &mut impl CryptoRngCore,
127+
previous_sieve: Option<&Self::Sieve>,
128+
) -> Option<Self::Sieve> {
129+
self.count += 1;
130+
if previous_sieve.is_none() {
131+
Some(self.count * 10..(self.count * 10 + 2))
132+
} else {
133+
None
134+
}
135+
}
136+
}
137+
138+
let factory = TestSieveFactory { count: 0 };
139+
let result = sieve_and_find(&mut OsRng, factory, |_rng, num| *num == 11);
140+
assert!(result.is_some());
141+
142+
#[cfg(feature = "multicore")]
143+
{
144+
let factory = TestSieveFactory { count: 0 };
145+
let result = par_sieve_and_find(&mut OsRng, factory, |_rng, num| *num == 11, 1);
146+
assert!(result.is_some());
147+
}
148+
149+
let factory = TestSieveFactory { count: 0 };
150+
let result = sieve_and_find(&mut OsRng, factory, |_rng, num| *num == 20);
151+
assert!(result.is_none());
152+
153+
#[cfg(feature = "multicore")]
154+
{
155+
let factory = TestSieveFactory { count: 0 };
156+
let result = par_sieve_and_find(&mut OsRng, factory, |_rng, num| *num == 20, 1);
157+
assert!(result.is_none());
158+
}
159+
}
160+
}

src/hazmat.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ mod sieve;
1414

1515
pub use lucas::{lucas_test, AStarBase, BruteForceBase, LucasBase, LucasCheck, SelfridgeBase};
1616
pub use miller_rabin::MillerRabin;
17-
pub use sieve::{random_odd_integer, Sieve};
17+
pub use sieve::{random_odd_integer, SmallPrimesSieve, SmallPrimesSieveFactory};
1818

1919
/// Possible results of various primality tests.
2020
#[derive(Copy, Clone, Debug, PartialEq, Eq)]

src/hazmat/miller_rabin.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,7 @@ mod tests {
147147
use num_prime::nt_funcs::is_prime64;
148148

149149
use super::MillerRabin;
150-
use crate::hazmat::{primes, pseudoprimes, random_odd_integer, Sieve};
150+
use crate::hazmat::{primes, pseudoprimes, random_odd_integer, SmallPrimesSieve};
151151

152152
#[test]
153153
fn miller_rabin_derived_traits() {
@@ -197,7 +197,7 @@ mod tests {
197197
fn trivial() {
198198
let mut rng = ChaCha8Rng::from_seed(*b"01234567890123456789012345678901");
199199
let start = random_odd_integer::<U1024>(&mut rng, NonZero::new(1024).unwrap());
200-
for num in Sieve::new(start.get(), NonZero::new(1024).unwrap(), false).take(10) {
200+
for num in SmallPrimesSieve::new(start.get(), NonZero::new(1024).unwrap(), false).take(10) {
201201
let mr = MillerRabin::new(Odd::new(num).unwrap());
202202

203203
// Trivial tests, must always be true.

0 commit comments

Comments
 (0)