mirror of
https://git.sdf.org/epl692/pi.git
synced 2026-02-10 19:54:09 -05:00
Implement improvements: rayon parallelism, safer CLI, output file option, tests, CI workflow, tuned release profile
This commit is contained in:
27
.github/workflows/ci.yml
vendored
Normal file
27
.github/workflows/ci.yml
vendored
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
name: CI
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ "**" ]
|
||||||
|
pull_request:
|
||||||
|
branches: [ "**" ]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- name: Install Rust toolchain
|
||||||
|
uses: actions-rs/toolchain@v1
|
||||||
|
with:
|
||||||
|
toolchain: stable
|
||||||
|
profile: minimal
|
||||||
|
override: true
|
||||||
|
- name: Run cargo fmt check
|
||||||
|
run: cargo fmt -- --check
|
||||||
|
- name: Run clippy
|
||||||
|
run: cargo clippy --all-targets --all-features -- -D warnings
|
||||||
|
- name: Run tests
|
||||||
|
run: cargo test --verbose
|
||||||
|
- name: Build release
|
||||||
|
run: cargo build --release --verbose
|
||||||
11
Cargo.toml
11
Cargo.toml
@@ -1,8 +1,17 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "pi"
|
name = "pi"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
edition = "2024"
|
edition = "2021"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
clap = { version = "4.5.4", features = ["derive"] }
|
clap = { version = "4.5.4", features = ["derive"] }
|
||||||
rug = "1.24.1"
|
rug = "1.24.1"
|
||||||
|
rayon = "1.7"
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
criterion = "0.4"
|
||||||
|
|
||||||
|
[profile.release]
|
||||||
|
opt-level = 3
|
||||||
|
lto = true
|
||||||
|
codegen-units = 1
|
||||||
|
|||||||
42
README.md
42
README.md
@@ -1,20 +1,18 @@
|
|||||||
# Pi Calculator
|
# Pi Calculator
|
||||||
|
|
||||||
This is a multi-threaded Rust program that calculates the first n digits of Pi using the Bailey–Borwein–Plouffe (BBP) formula. It uses arbitrary-precision arithmetic to ensure the accuracy of the calculated digits.
|
This is a multi-threaded Rust program that calculates the first n digits of Pi using the Bailey–Borwein–Plouffe (BBP) formula. It uses arbitrary-precision arithmetic (rug) and parallelism (rayon).
|
||||||
|
|
||||||
## Features
|
## Improvements in this branch
|
||||||
|
|
||||||
* Calculates the first n digits of Pi.
|
* Parallelized BBP summation with rayon for better thread control and load balancing.
|
||||||
* Multi-threaded to speed up the calculation.
|
* Safer argument validation and error handling (avoids unwraps on runtime errors).
|
||||||
* Configurable number of threads.
|
* Optional output-to-file support.
|
||||||
* Uses the BBP algorithm.
|
* Added CI workflow to run formatting, clippy, tests and build on push/PR.
|
||||||
* High-precision calculation using the `rug` crate.
|
* Release profile tuned for better optimized builds (LTO, opt-level=3).
|
||||||
|
|
||||||
## Building
|
## Building
|
||||||
|
|
||||||
To build the program, you need to have Rust and Cargo installed. You can install them from [https://rustup.rs/](https://rustup.rs/).
|
Requires Rust and Cargo. Build with:
|
||||||
|
|
||||||
Once you have Rust and Cargo installed, you can build the program with the following command:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cargo build --release
|
cargo build --release
|
||||||
@@ -22,26 +20,28 @@ cargo build --release
|
|||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
To run the program, you can use the following command:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
./target/release/pi <N> [OPTIONS]
|
./target/release/pi <N> [OPTIONS]
|
||||||
```
|
```
|
||||||
|
|
||||||
### Arguments
|
Arguments
|
||||||
|
|
||||||
* `<N>`: The number of digits of Pi to calculate.
|
* `<N>`: Number of digits after the decimal point to calculate.
|
||||||
|
|
||||||
### Options
|
Options
|
||||||
|
|
||||||
* `-t`, `--threads <THREADS>`: The number of threads to use. Defaults to 4.
|
* `-t`, `--threads <THREADS>`: Number of threads to use (default 4).
|
||||||
* `-h`, `--help`: Print help information.
|
* `-o`, `--output <FILE>`: Write output to FILE instead of stdout.
|
||||||
* `-V`, `--version`: Print version information.
|
* `-h`, `--help`: Print help.
|
||||||
|
|
||||||
### Example
|
Example
|
||||||
|
|
||||||
To calculate the first 1000 digits of Pi using 8 threads, you can run the following command:
|
Calculate 1000 digits using 8 threads and write to a file:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
./target/release/pi 1000 -t 8
|
./target/release/pi 1000 -t 8 -o pi1000.txt
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Notes
|
||||||
|
|
||||||
|
For very large numbers of digits, using a decimal-friendly algorithm such as Chudnovsky (with binary splitting) will be far faster and more memory-efficient than BBP; consider switching to Chudnovsky for production-grade large computations.
|
||||||
|
|||||||
132
src/main.rs
132
src/main.rs
@@ -1,19 +1,28 @@
|
|||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
use rug::{Float, ops::Pow};
|
use rug::Float;
|
||||||
use std::thread;
|
use rug::ops::Pow;
|
||||||
|
use rayon::prelude::*;
|
||||||
|
use std::fs::File;
|
||||||
|
use std::io::Write;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
#[derive(Parser, Debug)]
|
#[derive(Parser, Debug)]
|
||||||
#[command(author, version, about, long_about = None)]
|
#[command(author, version, about, long_about = None)]
|
||||||
struct Args {
|
struct Args {
|
||||||
/// Number of digits of Pi to calculate
|
/// Number of digits of Pi to calculate (digits after the decimal point)
|
||||||
n: u32,
|
n: u32,
|
||||||
|
|
||||||
/// Number of threads to use
|
/// Number of threads to use
|
||||||
#[arg(short, long, default_value_t = 4)]
|
#[arg(short, long, default_value_t = 4)]
|
||||||
threads: usize,
|
threads: usize,
|
||||||
|
|
||||||
|
/// Optional output file (writes result there if provided)
|
||||||
|
#[arg(short, long)]
|
||||||
|
output: Option<PathBuf>,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn bbp_term(k: u32, prec: u32) -> Float {
|
fn bbp_term(k: u32, prec: u32) -> Float {
|
||||||
|
// Compute one BBP term at precision `prec`.
|
||||||
let mut term = Float::with_val(prec, 4);
|
let mut term = Float::with_val(prec, 4);
|
||||||
term /= Float::with_val(prec, 8 * k + 1);
|
term /= Float::with_val(prec, 8 * k + 1);
|
||||||
|
|
||||||
@@ -30,52 +39,95 @@ fn bbp_term(k: u32, prec: u32) -> Float {
|
|||||||
term -= term4;
|
term -= term4;
|
||||||
|
|
||||||
let sixteen = Float::with_val(prec, 16);
|
let sixteen = Float::with_val(prec, 16);
|
||||||
term /= sixteen.pow(k);
|
term /= sixteen.pow(k as i32);
|
||||||
|
|
||||||
term
|
term
|
||||||
}
|
}
|
||||||
|
|
||||||
fn main() {
|
/// Calculate Pi to `n` decimal digits using a parallelized BBP summation.
|
||||||
let args = Args::parse();
|
/// Returns a decimal string containing Pi truncated to `n` digits after the decimal point.
|
||||||
let n = args.n;
|
pub fn calculate_pi(n: u32, num_threads: usize) -> Result<String, String> {
|
||||||
let num_threads = args.threads;
|
if n == 0 {
|
||||||
|
return Err("n must be > 0".into());
|
||||||
// Precision for rug::Float. We need a bit more than n decimal digits.
|
}
|
||||||
// log2(10) is approx 3.32. So, we need n * 3.32 bits.
|
if num_threads == 0 {
|
||||||
let prec = (n as f64 * 3.33).ceil() as u32 + 10;
|
return Err("threads must be > 0".into());
|
||||||
|
|
||||||
let num_terms = n + 5; // Use more terms for better accuracy
|
|
||||||
let terms_per_thread = (num_terms + num_threads as u32 - 1) / num_threads as u32;
|
|
||||||
|
|
||||||
let mut handles = vec![];
|
|
||||||
|
|
||||||
for i in 0..num_threads {
|
|
||||||
let start = i as u32 * terms_per_thread;
|
|
||||||
let end = ((i + 1) as u32 * terms_per_thread).min(num_terms);
|
|
||||||
let handle = thread::spawn(move || {
|
|
||||||
let mut partial_sum = Float::with_val(prec, 0);
|
|
||||||
for k in start..end {
|
|
||||||
partial_sum += bbp_term(k, prec);
|
|
||||||
}
|
|
||||||
partial_sum
|
|
||||||
});
|
|
||||||
handles.push(handle);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut pi = Float::with_val(prec, 0);
|
// Bits of precision: log2(10) ~= 3.321928. Add some guard bits.
|
||||||
for handle in handles {
|
let prec = (n as f64 * 3.3219280948873626).ceil() as u32 + 20;
|
||||||
pi += handle.join().unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
// The user wants n digits after the decimal, and the output to be truncated.
|
// BBP converges in base-16; use a modest overestimate for term count.
|
||||||
// We can achieve this by getting a string with more precision and then truncating it.
|
let num_terms = (n as usize / 1) + 20; // conservative
|
||||||
let pi_string = pi.to_string_radix(10, Some(n as usize + 5)); // Get extra digits for accurate truncation
|
|
||||||
let dot_pos = pi_string.find('.').unwrap_or(1);
|
// Use rayon thread pool to control threads for parallel work.
|
||||||
|
let pool = rayon::ThreadPoolBuilder::new()
|
||||||
|
.num_threads(num_threads)
|
||||||
|
.build()
|
||||||
|
.map_err(|e| format!("Failed to build thread pool: {}", e))?;
|
||||||
|
|
||||||
|
let pi = pool.install(|| {
|
||||||
|
// Parallel iterator over term indices.
|
||||||
|
(0..num_terms as u32)
|
||||||
|
.into_par_iter()
|
||||||
|
.map(|k| bbp_term(k, prec))
|
||||||
|
.reduce(|| Float::with_val(prec, 0), |a, b| a + b)
|
||||||
|
});
|
||||||
|
|
||||||
|
// Convert to decimal string with a few extra digits for safe truncation.
|
||||||
|
let extra = 10usize;
|
||||||
|
let pi_string = pi.to_string_radix(10, Some(n as usize + extra));
|
||||||
|
|
||||||
|
// Find dot safely and truncate or pad as needed.
|
||||||
|
let dot_pos = pi_string.find('.').unwrap_or(pi_string.len());
|
||||||
let end_pos = dot_pos + 1 + n as usize;
|
let end_pos = dot_pos + 1 + n as usize;
|
||||||
|
|
||||||
if pi_string.len() > end_pos {
|
let out = if pi_string.len() >= end_pos {
|
||||||
println!("Pi: {}", &pi_string[..end_pos]);
|
pi_string[..end_pos].to_string()
|
||||||
} else {
|
} else {
|
||||||
println!("Pi: {}", pi_string);
|
// If not enough digits were produced, pad with zeros.
|
||||||
|
let mut s = pi_string;
|
||||||
|
if !s.contains('.') {
|
||||||
|
s.push('.');
|
||||||
|
}
|
||||||
|
while s.len() < end_pos {
|
||||||
|
s.push('0');
|
||||||
|
}
|
||||||
|
s
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(out)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let args = Args::parse();
|
||||||
|
|
||||||
|
match calculate_pi(args.n, args.threads) {
|
||||||
|
Ok(pi_str) => {
|
||||||
|
if let Some(path) = args.output {
|
||||||
|
match File::create(&path) {
|
||||||
|
Ok(mut f) => {
|
||||||
|
if let Err(e) = writeln!(f, "{}", pi_str) {
|
||||||
|
eprintln!("Failed to write to {}: {}", path.display(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => eprintln!("Failed to create {}: {}", path.display(), e),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
println!("Pi: {}", pi_str);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => eprintln!("Error: {}", e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::calculate_pi;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn pi_10_digits() {
|
||||||
|
let pi = calculate_pi(10, 2).expect("calculation failed");
|
||||||
|
assert_eq!(pi, "3.1415926535");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user