I've implemented the prime sieve of Eratosthenes in Haskell using a mutable array of unboxed Bools instead of the usual pure and immutable datastructures.
My main concern with this code is speed. For large upper bounds, it runs approximately 70% slower than C code implementing the same algorithm. Memory is already optimal (1 bit per number plus some small overhead).
I would also like to know if the code is reasonably idiomatic, considering the fact that is impure, and basically non-functional (meaning imperative). Any nitpicks are appreciated.
import Prelude
import System.Environment (getArgs)
import Data.Array.Unboxed (UArray, (!))
import Data.Array.Storable (newArray, readArray, writeArray, getBounds)
import Data.Array.ST (STUArray, runSTUArray)
import Control.Monad (when)
import Control.Monad.ST (ST)
markMultiplesAsNotPrime :: STUArray s Int Bool -> Int -> ST s ()
markMultiplesAsNotPrime isPrime k = do
(_, n) <- getBounds isPrime
kIsPrime <- readArray isPrime k
when kIsPrime $ do
let multiples = takeWhile (<=n) $ iterate (+k) (k+k)
mapM_ (\i -> writeArray isPrime i False) multiples
sieve :: Int -> UArray Int Bool
sieve n | n < 0 = undefined
sieve n = runSTUArray $ do
isPrime <- newArray (2, n) True :: ST s (STUArray s Int Bool)
let kMax = isqrt' n
let ks = takeWhile (<=kMax) . iterate (+1) $ 2
mapM_ (markMultiplesAsNotPrime isPrime) ks
return isPrime
isqrt' :: Int -> Int
isqrt' = ceiling . (sqrt :: Double -> Double) . fromIntegral
primesSmallerThanOrEqualTo :: Int -> [Int]
primesSmallerThanOrEqualTo n | n < 2 = []
primesSmallerThanOrEqualTo n = let
isPrime = sieve n
in
[i | i <- [2 .. n], isPrime ! i]
main :: IO ()
main = do
args <- getArgs
mapM_ (print . last . primesSmallerThanOrEqualTo . read) args
In case it is relevant: I compiled and executed the code on an amd64 CPU using "The Glorious Glasgow Haskell Compilation System, version 7.10.3" with the optimization switch "-O2".
Some timings (as measured with the runtime system switch "-s", best of five runs): $$ \begin{array}{4c} n & π(n) & t [s] & \log\left(\frac{t_2}{t_1}\right) \bigg/ \log\left(\frac{π(n_2)}{π(n_1)}\right) \\ 10^4 & 1.23 \cdot 10^3 & 2 \cdot 10^{-3} & - \\ 10^5 & 9.59 \cdot 10^3 & 4 \cdot 10^{-3} & 0.3 \\ 10^6 & 7.85 \cdot 10^4 & 1.0 \cdot 10^{-2} & 0.4 \\ 10^7 & 6.65 \cdot 10^5 & 9.3 \cdot 10^{-2} & 1.04 \\ 10^8 & 5.76 \cdot 10^6 & 1.25 \cdot 10^0 & 1.20 \\ 10^9 & 5.08 \cdot 10^7 & 1.43 \cdot 10^1 & 1.12 \\ 10^{10} & 4.55 \cdot 10^8 & 1.61 \cdot 10^2 & 1.10 \\ 10^{11} & 4.12 \cdot 10^9 & 1.88 \cdot 10^3 & 1.12 \end{array} $$
+RTS -s
switch to see some statistics report, including time. Anything below ~ n^1.05 is very good, below ~n^1.10 -- pretty decent (measuring it inn
primes produced). \$\endgroup\$