How to optimize this Haskell code summing up the primes in sublinear time?

后端 未结 4 1422
迷失自我
迷失自我 2021-02-18 23:11

Problem 10 from Project Euler is to find the sum of all the primes below given n.

I solved it simply by summing up the primes generated by the sieve of Eratosth

4条回答
  •  星月不相逢
    2021-02-18 23:37

    I've done some small improvements so it runs in 3.4-3.5 seconds on my machine. Using IntMap.Strict helped a lot. Other than that I just manually performed some ghc optimizations just to be sure. And make Haskell code more close to Python code from your link. As a next step you could try to use some mutable HashMap. But I'm not sure... IntMap can't be much faster than some mutable container because it's an immutable one. Though I'm still surprised about it's efficiency. I hope this can be implemented faster.

    Here is the code:

    import Data.List (foldl')
    import Data.IntMap.Strict (IntMap, (!))
    import qualified Data.IntMap.Strict as IntMap
    
    p :: Int -> Int
    p n = (sieve (IntMap.fromList [(i, i * (i + 1) `div` 2 - 1) | i <- vs]) 2 r vs) ! n
                   where vs = [n `div` i | i <- [1..r]] ++ [n', n' - 1 .. 1]
                         r  = floor (sqrt (fromIntegral n) :: Double)
                         n' = n `div` r - 1
    
    sieve :: IntMap Int -> Int -> Int -> [Int] -> IntMap Int
    sieve m' p' r vs = go m' p'
      where
        go m p | p > r               = m
               | m ! p > m ! (p - 1) = go (update m vs p) (p + 1)
               | otherwise           = go m (p + 1)
    
    update :: IntMap Int -> [Int] -> Int -> IntMap Int
    update s vs p = foldl' decrease s (takeWhile (>= p2) vs)
      where
        sp = s ! (p - 1)
        p2 = p * p
        sumOfSieved v = p * (s ! (v `div` p) - sp)
        decrease m  v = IntMap.adjust (subtract $ sumOfSieved v) v m
    
    main :: IO ()
    main = print $ p $ 2*10^(9 :: Int) 
    

    UPDATE:

    Using mutable hashtables I've managed to make performance up to ~5.5sec on Haskell with this implementation.

    Also, I used unboxed vectors instead of lists in several places. Linear hashing seems to be the fastest. I think this can be done even faster. I noticed sse42 option in hasthables package. Not sure I've managed to set it correctly but even without it runs that fast.

    UPDATE 2 (19.06.2017)

    I've managed to make it 3x faster then best solution from @Krom (using my code + his map) by dropping judy hashmap at all. Instead just plain arrays are used. You can come up with the same idea if you notice that keys for S hashmap are either sequence from 1 to n' or n div i for i from 1 to r. So we can represent such HashMap as two arrays making lookups in array depending on searching key.

    My code + Judy HashMap

    $ time ./judy
    95673602693282040
    
    real    0m0.590s
    user    0m0.588s
    sys     0m0.000s
    

    My code + my sparse map

    $ time ./sparse
    95673602693282040
    
    real    0m0.203s
    user    0m0.196s
    sys     0m0.004s
    

    This can be done even faster if instead of IOUArray already generated vectors and Vector library is used and readArray is replaced by unsafeRead. But I don't think this should be done if only you're not really interested in optimizing this as much as possible.

    Comparison with this solution is cheating and is not fair. I expect same ideas implemented in Python and C++ will be even faster. But @Krom solution with closed hashmap is already cheating because it uses custom data structure instead of standard one. At least you can see that standard and most popular hash maps in Haskell are not that fast. Using better algorithms and better ad-hoc data structures can be better for such problems.

    Here's resulting code.

提交回复
热议问题