Skip to content

Instantly share code, notes, and snippets.

@stefanwuthrich
Created April 4, 2025 07:50
Show Gist options
  • Save stefanwuthrich/d1b36f600060a311d317fe1cf0cbd462 to your computer and use it in GitHub Desktop.
Save stefanwuthrich/d1b36f600060a311d317fe1cf0cbd462 to your computer and use it in GitHub Desktop.
package main
import (
"fmt"
"math/big"
"runtime"
"sync"
"time"
)
// Calculate Pi using the Bailey-Borwein-Plouffe formula
// This formula allows direct computation of any hexadecimal digit of Pi
func calculatePiBBP(digits int) string {
// Set precision high enough for the calculation
precision := uint(digits*4 + 100)
// Initialize result
pi := new(big.Float).SetPrec(precision)
pi.SetInt64(0)
// Constants
one := new(big.Float).SetPrec(precision)
one.SetInt64(1)
sixteen := new(big.Float).SetPrec(precision)
sixteen.SetInt64(16)
// Temporary variables
term := new(big.Float).SetPrec(precision)
sum := new(big.Float).SetPrec(precision)
power := new(big.Float).SetPrec(precision)
// Number of terms needed for the desired precision
terms := digits * 2
// Calculate Pi using the BBP formula
// Pi = sum_{k=0}^infinity (1/16^k) * (4/(8k+1) - 2/(8k+4) - 1/(8k+5) - 1/(8k+6))
for k := 0; k < terms; k++ {
// Calculate 1/16^k
power.SetInt64(1)
for i := 0; i < k; i++ {
power.Quo(power, sixteen)
}
// Calculate the sum for this k
sum.SetInt64(0)
// 4/(8k+1)
term.SetInt64(4)
term.Quo(term, new(big.Float).SetInt64(int64(8*k+1)))
sum.Add(sum, term)
// -2/(8k+4)
term.SetInt64(2)
term.Quo(term, new(big.Float).SetInt64(int64(8*k+4)))
sum.Sub(sum, term)
// -1/(8k+5)
term.SetInt64(1)
term.Quo(term, new(big.Float).SetInt64(int64(8*k+5)))
sum.Sub(sum, term)
// -1/(8k+6)
term.SetInt64(1)
term.Quo(term, new(big.Float).SetInt64(int64(8*k+6)))
sum.Sub(sum, term)
// Multiply by 1/16^k and add to pi
sum.Mul(sum, power)
pi.Add(pi, sum)
}
// Convert to string with the desired precision
return pi.Text('f', digits)
}
// Parallel version of the BBP formula
func calculatePiBBPParallel(digits int) string {
// Set precision high enough for the calculation
precision := uint(digits*4 + 100)
// Number of terms needed for the desired precision
terms := digits * 2
// Determine number of goroutines based on CPU cores
numCPU := runtime.NumCPU()
numWorkers := numCPU * 2 // Use 2x CPU cores for better utilization
// Ensure we don't create more workers than terms
if numWorkers > terms {
numWorkers = terms
}
// Calculate terms per worker
termsPerWorker := terms / numWorkers
// Create a channel for results
results := make(chan *big.Float, numWorkers)
// Use a wait group to synchronize goroutines
wg := sync.WaitGroup{}
// Launch worker goroutines
for w := 0; w < numWorkers; w++ {
wg.Add(1)
// Calculate the range for this worker
startK := w * termsPerWorker
endK := (w + 1) * termsPerWorker
// Last worker takes any remaining terms
if w == numWorkers-1 {
endK = terms
}
// Launch the worker goroutine
go func(start, end int) {
defer wg.Done()
// Initialize worker's partial sum
partialSum := new(big.Float).SetPrec(precision)
partialSum.SetInt64(0)
// Constants
sixteen := new(big.Float).SetPrec(precision)
sixteen.SetInt64(16)
// Temporary variables
term := new(big.Float).SetPrec(precision)
sum := new(big.Float).SetPrec(precision)
power := new(big.Float).SetPrec(precision)
// Calculate terms in this worker's range
for k := start; k < end; k++ {
// Calculate 1/16^k
power.SetInt64(1)
for i := 0; i < k; i++ {
power.Quo(power, sixteen)
}
// Calculate the sum for this k
sum.SetInt64(0)
// 4/(8k+1)
term.SetInt64(4)
term.Quo(term, new(big.Float).SetInt64(int64(8*k+1)))
sum.Add(sum, term)
// -2/(8k+4)
term.SetInt64(2)
term.Quo(term, new(big.Float).SetInt64(int64(8*k+4)))
sum.Sub(sum, term)
// -1/(8k+5)
term.SetInt64(1)
term.Quo(term, new(big.Float).SetInt64(int64(8*k+5)))
sum.Sub(sum, term)
// -1/(8k+6)
term.SetInt64(1)
term.Quo(term, new(big.Float).SetInt64(int64(8*k+6)))
sum.Sub(sum, term)
// Multiply by 1/16^k and add to partial sum
sum.Mul(sum, power)
partialSum.Add(partialSum, sum)
}
// Send the partial sum to the results channel
results <- partialSum
}(startK, endK)
}
// Wait for all workers to complete
go func() {
wg.Wait()
close(results)
}()
// Combine the results
pi := new(big.Float).SetPrec(precision)
pi.SetInt64(0)
for partialSum := range results {
pi.Add(pi, partialSum)
}
// Convert to string with the desired precision
return pi.Text('f', digits)
}
// Calculate Pi using the Machin formula
// Pi = 16*arctan(1/5) - 4*arctan(1/239)
func calculatePiMachin(digits int) string {
// Set precision high enough for the calculation
precision := uint(digits*4 + 100)
// Initialize result with high precision
pi := new(big.Float).SetPrec(precision)
pi.SetInt64(0)
// Calculate 16*arctan(1/5)
arctan1 := arctan(new(big.Float).SetInt64(5), precision, digits)
arctan1.Mul(arctan1, new(big.Float).SetInt64(16))
// Calculate 4*arctan(1/239)
arctan2 := arctan(new(big.Float).SetInt64(239), precision, digits)
arctan2.Mul(arctan2, new(big.Float).SetInt64(4))
// Pi = 16*arctan(1/5) - 4*arctan(1/239)
pi.Sub(arctan1, arctan2)
// Convert to string with the desired precision
return pi.Text('f', digits)
}
// Parallel version of the Machin formula
func calculatePiMachinParallel(digits int) string {
// Set precision high enough for the calculation
precision := uint(digits*4 + 100)
// Initialize result with high precision
pi := new(big.Float).SetPrec(precision)
pi.SetInt64(0)
// Use a wait group to synchronize goroutines
wg := sync.WaitGroup{}
// Create channels for results
arctan1Chan := make(chan *big.Float, 1)
arctan2Chan := make(chan *big.Float, 1)
// Calculate 16*arctan(1/5) in a goroutine
wg.Add(1)
go func() {
defer wg.Done()
result := arctanParallel(new(big.Float).SetInt64(5), precision, digits)
result.Mul(result, new(big.Float).SetInt64(16))
arctan1Chan <- result
}()
// Calculate 4*arctan(1/239) in a goroutine
wg.Add(1)
go func() {
defer wg.Done()
result := arctanParallel(new(big.Float).SetInt64(239), precision, digits)
result.Mul(result, new(big.Float).SetInt64(4))
arctan2Chan <- result
}()
// Wait for both calculations to complete
go func() {
wg.Wait()
close(arctan1Chan)
close(arctan2Chan)
}()
// Get the results
arctan1 := <-arctan1Chan
arctan2 := <-arctan2Chan
// Pi = 16*arctan(1/5) - 4*arctan(1/239)
pi.Sub(arctan1, arctan2)
// Convert to string with the desired precision
return pi.Text('f', digits)
}
// Calculate arctan(1/x) to the specified precision
func arctan(x *big.Float, precision uint, digits int) *big.Float {
// Initialize result with high precision
result := new(big.Float).SetPrec(precision)
result.SetInt64(0)
// Temporary variables
term := new(big.Float).SetPrec(precision)
term.SetInt64(1)
term.Quo(term, x)
xSquared := new(big.Float).SetPrec(precision)
xSquared.Mul(x, x)
// Number of terms needed for the desired precision
terms := digits * 2
// Calculate arctan(1/x) using the series: arctan(1/x) = 1/x - 1/(3*x^3) + 1/(5*x^5) - ...
for k := 0; k < terms; k++ {
// Add or subtract the term based on whether k is even or odd
if k%2 == 0 {
result.Add(result, term)
} else {
result.Sub(result, term)
}
// Calculate the next term: term = term / (x^2 * (2k+3)/(2k+1))
term.Quo(term, xSquared)
term.Mul(term, new(big.Float).SetInt64(int64(2*k + 1)))
term.Quo(term, new(big.Float).SetInt64(int64(2*k + 3)))
}
return result
}
// Parallel version of arctan calculation
func arctanParallel(x *big.Float, precision uint, digits int) *big.Float {
// Initialize result with high precision
result := new(big.Float).SetPrec(precision)
result.SetInt64(0)
// Number of terms needed for the desired precision
terms := digits * 2
// Determine number of goroutines based on CPU cores
numCPU := runtime.NumCPU()
numWorkers := numCPU * 2 // Use 2x CPU cores for better utilization
// Ensure we don't create more workers than terms
if numWorkers > terms {
numWorkers = terms
}
// Calculate terms per worker
termsPerWorker := terms / numWorkers
// Create a channel for results
results := make(chan *big.Float, numWorkers)
// Use a wait group to synchronize goroutines
wg := sync.WaitGroup{}
// Launch worker goroutines
for w := 0; w < numWorkers; w++ {
wg.Add(1)
// Calculate the range for this worker
startK := w * termsPerWorker
endK := (w + 1) * termsPerWorker
// Last worker takes any remaining terms
if w == numWorkers-1 {
endK = terms
}
// Launch the worker goroutine
go func(start, end int) {
defer wg.Done()
// Initialize worker's partial sum
partialSum := new(big.Float).SetPrec(precision)
partialSum.SetInt64(0)
// Calculate x^2 for the series
xSquared := new(big.Float).SetPrec(precision)
xSquared.Mul(x, x)
// For each term in this worker's range
for k := start; k < end; k++ {
// Calculate the term
term := new(big.Float).SetPrec(precision)
term.SetInt64(1)
term.Quo(term, x)
// Apply x^(2k) to the denominator
for i := 0; i < k; i++ {
term.Quo(term, xSquared)
}
// Apply the factorial ratio (2k+1)/(2k+3)/(2k+5)/...
for i := 0; i < k; i++ {
term.Mul(term, new(big.Float).SetInt64(int64(2*i + 1)))
term.Quo(term, new(big.Float).SetInt64(int64(2*i + 3)))
}
// Apply sign based on whether k is even or odd
if k%2 == 1 {
term.Neg(term)
}
// Add to partial sum
partialSum.Add(partialSum, term)
}
// Send the partial sum to the results channel
results <- partialSum
}(startK, endK)
}
// Wait for all workers to complete
go func() {
wg.Wait()
close(results)
}()
// Combine the results
for partialSum := range results {
result.Add(result, partialSum)
}
return result
}
// Known value of Pi to 1000 decimal places for verification
const knownPi = "3." +
"1415926535897932384626433832795028841971693993751058209749445923078164062862089986280348253421170679" +
"8214808651328230664709384460955058223172535940812848111745028410270193852110555964462294895493038196" +
"4428810975665933446128475648233786783165271201909145648566923460348610454326648213393607260249141273" +
"7245870066063155881748815209209628292540917153643678925903600113305305488204665213841469519415116094" +
"3305727036575959195309218611738193261179310511854807446237996274956735188575272489122793818301194912" +
"9833673362440656643086021394946395224737190702179860943702770539217176293176752384674818467669405132" +
"0005681271452635608277857713427577896091736371787214684409012249534301465495853710507922796892589235" +
"4201995611212902196086403441815981362977477130996051870721134999999837297804995105973173281609631859" +
"5024459455346908302642522308253344685035261931188171010003137838752886587533208381420617177669147303" +
"5982534904287554687311595628638823537875937519577818577805321712268066130019278766111959092164201989"
func main() {
digits := 500
// Get number of CPU cores for information
numCPU := runtime.NumCPU()
fmt.Printf("Using %d CPU cores\n", numCPU)
// Calculate Pi using the BBP formula (sequential)
start := time.Now()
piBBP := calculatePiBBP(digits)
durationBBP := time.Since(start)
// Calculate Pi using the BBP formula (parallel)
start = time.Now()
piBBPParallel := calculatePiBBPParallel(digits)
durationBBPParallel := time.Since(start)
// Calculate Pi using the Machin formula (sequential)
start = time.Now()
piMachin := calculatePiMachin(digits)
durationMachin := time.Since(start)
// Calculate Pi using the Machin formula (parallel)
start = time.Now()
piMachinParallel := calculatePiMachinParallel(digits)
durationMachinParallel := time.Since(start)
// Choose the result to display
var piStr string
var algorithm string
var duration time.Duration
// Use the parallel BBP formula result as it's generally fastest and accurate
piStr = piBBPParallel
algorithm = "Parallel Bailey-Borwein-Plouffe"
duration = durationBBPParallel
fmt.Printf("Pi with %d decimal places (using %s algorithm):\n", digits, algorithm)
fmt.Println(piStr)
fmt.Printf("\nCalculation took %v\n", duration)
// Verify against known value
fmt.Println("\nVerification of first 100 digits:")
fmt.Println("Known Pi: ", knownPi[:102])
fmt.Println("Computed Pi: ", piStr[:102])
if piStr[:102] == knownPi[:102] {
fmt.Println("✓ First 100 digits match!")
} else {
fmt.Println("✗ Digits do not match!")
// Find where the digits start to differ
for i := 0; i < 102 && i < len(piStr) && i < len(knownPi); i++ {
if piStr[i] != knownPi[i] {
fmt.Printf("Digits differ at position %d: expected '%c', got '%c'\n",
i, knownPi[i], piStr[i])
break
}
}
}
// Show performance comparison
fmt.Println("\nPerformance Comparison:")
fmt.Printf("1. Sequential BBP: %v\n", durationBBP)
fmt.Printf("2. Parallel BBP: %v (%.1fx speedup)\n",
durationBBPParallel, float64(durationBBP.Nanoseconds())/float64(durationBBPParallel.Nanoseconds()))
fmt.Printf("3. Sequential Machin: %v\n", durationMachin)
fmt.Printf("4. Parallel Machin: %v (%.1fx speedup)\n",
durationMachinParallel, float64(durationMachin.Nanoseconds())/float64(durationMachinParallel.Nanoseconds()))
// Verify that all methods produce the same result
fmt.Println("\nVerifying that all methods produce the same result:")
if piBBP[:102] == piBBPParallel[:102] {
fmt.Println("✓ Sequential and parallel BBP match!")
} else {
fmt.Println("✗ Sequential and parallel BBP differ!")
}
if piMachin[:102] == piMachinParallel[:102] {
fmt.Println("✓ Sequential and parallel Machin match!")
} else {
fmt.Println("✗ Sequential and parallel Machin differ!")
}
if piBBPParallel[:102] == piMachinParallel[:102] {
fmt.Println("✓ BBP and Machin algorithms match!")
} else {
fmt.Println("✗ BBP and Machin algorithms differ!")
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment