From f9c72ed0a77ea1bd23129a8dce5cf0491c860275 Mon Sep 17 00:00:00 2001 From: Stefan Lindblad Date: Mon, 6 Nov 2023 21:58:28 +0100 Subject: [PATCH] Restructure and update Break out tests and prime wheel generator to separate files. Upgrade to latest dependencies, except for clap. Improve section about memory efficiency in the README. --- .gitignore | 1 + Cargo.toml | 18 ++++----- README.md | 2 +- src/candidates.rs | 20 ++++++++++ src/lib.rs | 88 +++-------------------------------------- tests/test.rs | 99 ++++++++++++++++++++++++++++++++++++++++------- 6 files changed, 120 insertions(+), 108 deletions(-) create mode 100644 src/candidates.rs diff --git a/.gitignore b/.gitignore index 1e7caa9..ac671d8 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ Cargo.lock target/ +.idea/ diff --git a/Cargo.toml b/Cargo.toml index dbf08c2..56973ab 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "prime-factor" description = "A prime number factorizer written in Rust" -version = "0.4.3" +version = "0.4.4" authors = ["Stefan Lindblad "] license = "LGPL-2.1-or-later" edition = "2021" @@ -23,16 +23,16 @@ path = "benches/benchmark.rs" harness = false [dependencies] -clap = "2.33.3" -log = "0.4.13" -stderrlog = "0.5.1" -genawaiter = "0.99.1" -rayon = "1.5.1" +clap = "2.34" +log = "0.4" +stderrlog = "0.5" +genawaiter = "0.99" +rayon = "1.8" [dev-dependencies] -criterion = "0.3.5" -rand = { version = "0.8.3", features = ["small_rng"] } -reikna = "0.12.3" +criterion = "0.5" +rand = { version = "0.8", features = ["small_rng"] } +reikna = "0.12" [profile.release] codegen-units = 1 diff --git a/README.md b/README.md index 2bb148e..8bee9b1 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ The library will calculate all the prime number factors of any 128-bit unsigned ## Memory efficiency -Main memory accesses are typically quite slow[^1] and a lot of the algorithms for calculating prime numbers use quite a lot of memory, the cache helps but not enough. During a single main memory load there is time for in the order of hundreds of calculations, which will be wasted cycles unless we can utilize them somehow. This means that we can get an efficient algorithm even with a bit of wasted computations if we can limit the number of memory operations. +A lot of prime number algorithms require a significant amount of memory, but accessing main memory can be a slow process[^1]. While the cache can provide some assistance, it may not be sufficient. With each load from main memory, there is typically enough time for up to hundreds of calculations. These cycles would be wasted, unless we can find some work to do while waiting for the load. Therefore, even with some amount of wasted computations, we can still achieve an efficient algorithm if we can minimize the number of memory operations. In my personal experience any algorithm that needs to store a lot of data will be limited by the memory accesses, it is often faster to recreate some computations rather than loading them from memory. Therefore do not save values to memory that can be easily computed. diff --git a/src/candidates.rs b/src/candidates.rs new file mode 100644 index 0000000..19e3f95 --- /dev/null +++ b/src/candidates.rs @@ -0,0 +1,20 @@ +#![deny(unsafe_code)] +#![allow(dead_code)] +use genawaiter::yield_; +use genawaiter::stack::producer_fn; + +/// Wheel factorization algorithm with base {2, 3, 5} +/// https://en.wikipedia.org/wiki/Wheel_factorization +#[producer_fn(u128)] +pub async fn prime_wheel_30() { + yield_!(2); + yield_!(3); + yield_!(5); + let mut base = 0u128; + loop { + for n in [7, 11, 13, 17, 19, 23, 29, 31] { + yield_!(base + n); + } + base += 30; + } +} diff --git a/src/lib.rs b/src/lib.rs index 618eaf4..cd15001 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,25 +1,11 @@ //! Module for factorizing integers +pub mod candidates; + use std::cmp::{min, Ordering}; use std::convert::From; use std::fmt; -use genawaiter::yield_; -use genawaiter::stack::{let_gen_using, producer_fn}; - -/// Wheel factorization algorithm with base {2, 3, 5} -/// https://en.wikipedia.org/wiki/Wheel_factorization -#[producer_fn(u128)] -async fn prime_wheel_30() { - yield_!(2); - yield_!(3); - yield_!(5); - let mut base = 0u128; - loop { - for n in [7, 11, 13, 17, 19, 23, 29, 31] { - yield_!(base + n); - } - base += 30; - } -} +use genawaiter::stack::let_gen_using; +use candidates::prime_wheel_30; #[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)] pub struct IntFactor { @@ -50,7 +36,7 @@ pub struct PrimeFactors { impl PrimeFactors { fn new() -> Self { - PrimeFactors { factors: Vec::new() } + PrimeFactors { factors: Vec::with_capacity(8) } } fn add(&mut self, integer: u128, exponent: u32) { self.factors.push(IntFactor { integer, exponent }) @@ -243,67 +229,3 @@ pub fn u128_lcm(this: u128, that: u128) -> u128 { let gcd = u128_gcd(this, that); this * that / gcd } - -#[cfg(test)] -mod tests { - #[allow(unused_imports)] - use crate::*; - use rand::Rng; - use genawaiter::stack::let_gen_using; - - #[test] - fn test_early_prime_wheel_numbers() { - let testvec = vec![ - 2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 49, 53, 59, - 61, 67, 71, 73, 77, 79, 83, 89, 91, 97, 101, 103, 107, 109, 113 - ]; - let_gen_using!(mpgen, prime_wheel_30); - let mut mp = mpgen.into_iter(); - for i in 0..testvec.len() { - let p = mp.next().unwrap(); - assert_eq!(testvec[i], p); - } - } - - #[test] - fn test_prime_wheel_quality() { - let mut primes: u128 = 0; - let mut others: u128 = 0; - let_gen_using!(mpgen, prime_wheel_30); - let mut mp = mpgen.into_iter(); - for _ in 0..1000000 { - let p = mp.next().unwrap(); - if u128_is_prime(p) { - primes += 1; - } else { - others += 1; - } - } - let percent = primes as f64 / (primes + others) as f64 * 100.0; - println!("Prime wheel generated {}/{} ({:.3}%) primes", - primes, primes+others, percent); - assert!(percent > 25.0); - } - - #[test] - fn test_int_sqrt_pow_of_2() { - let mut rnd = rand::thread_rng(); - for _ in 1..1000 { - let n = rnd.gen_range(1..u128_sqrt(u128::MAX)); - let sqrt = u128_sqrt(n.pow(2)); - assert_eq!(sqrt, n); - } - } - - #[test] - fn test_int_sqrt_floor() { - let mut rnd = rand::thread_rng(); - for _ in 1..1000 { - // Largest integer in a f64 is 2^53-1 (52 bits mantissa) - let n = rnd.gen_range(1..u64::pow(2, 53) as u128); - let expt = f64::sqrt(n as f64) as u128; - let sqrt = u128_sqrt(n); - assert_eq!(sqrt, expt); - } - } -} diff --git a/tests/test.rs b/tests/test.rs index d3369af..fdb7a69 100644 --- a/tests/test.rs +++ b/tests/test.rs @@ -1,9 +1,87 @@ #[allow(unused_imports)] -use primefactor::*; use rand::Rng; use rayon::prelude::*; use reikna; - +use genawaiter::stack::let_gen_using; +use primefactor::{ + candidates::prime_wheel_30, + primefactor_gcd, + PrimeFactors, + u128_gcd, + u128_is_prime, + u128_lcm, + u128_sqrt}; + +#[test] +fn test_early_prime_wheel_numbers() { + let testvec = vec![ + 2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 49, 53, 59, + 61, 67, 71, 73, 77, 79, 83, 89, 91, 97, 101, 103, 107, 109, 113 + ]; + let_gen_using!(mpgen, prime_wheel_30); + let mut mp = mpgen.into_iter(); + for i in 0..testvec.len() { + let p = mp.next().unwrap(); + assert_eq!(testvec[i], p); + } +} + +#[test] +fn test_early_prime_candidate_numbers() { + let testvec = vec![ + 2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 49, 53, 59, + 61, 67, 71, 73, 77, 79, 83, 89, 91, 97, 101, 103, 107, 109, 113 + ]; + let_gen_using!(mpgen, prime_wheel_30); + let mut mp = mpgen.into_iter(); + for i in 0..testvec.len() { + let p = mp.next().unwrap(); + assert_eq!(testvec[i], p); + } +} + +#[test] +fn test_prime_wheel_quality() { + let mut primes: u128 = 0; + let mut others: u128 = 0; + let_gen_using!(mpgen, prime_wheel_30); + let mut mp = mpgen.into_iter(); + for _ in 0..1000000 { + let p = mp.next().unwrap(); + if u128_is_prime(p) { + primes += 1; + } else { + others += 1; + } + } + let percent = primes as f64 / (primes + others) as f64 * 100.0; + println!("Prime wheel generated {}/{} ({:.3}%) primes", + primes, primes+others, percent); + assert!(percent > 25.0); +} + +#[test] +fn test_int_sqrt_pow_of_2() { + let mut rnd = rand::thread_rng(); + for _ in 1..1000 { + let n = rnd.gen_range(1..u128_sqrt(u128::MAX)); + let sqrt = u128_sqrt(n.pow(2)); + assert_eq!(sqrt, n); + } +} + +#[test] +fn test_int_sqrt_floor() { + let mut rnd = rand::thread_rng(); + for _ in 1..1000 { + // Largest integer in a f64 is 2^53-1 (52 bits mantissa) + let n = rnd.gen_range(1..u64::pow(2, 53) as u128); + let expt = f64::sqrt(n as f64) as u128; + let sqrt = u128_sqrt(n); + assert_eq!(sqrt, expt); + } +} + #[test] fn test_is_prime() { for num in 2..=1000 { @@ -142,18 +220,9 @@ fn find_highest_32bit_prime() { assert_eq!(found, 4294967291); } - #[test] -fn find_highest_64bit_prime() { - (0..60).into_par_iter().for_each(|n| { - let num: u128 = (u64::MAX - n) as u128; - match n { - 58 => { - assert_eq!(num, 18446744073709551557); - assert_eq!(u128_is_prime(num), true); - println!("#{}: {} is a prime number", n, num); - }, - _ => assert_eq!(u128_is_prime(num), false), - } - }); +fn worst_case_64bit_prime() { + let num: u128 = (u64::MAX - 58) as u128; + assert_eq!(num, 18446744073709551557); + assert_eq!(u128_is_prime(num), true); }