algorithm - 如何优化这个 Haskell 代码在亚线性时间内总结素数?

标签 algorithm haskell optimization data-structures primes

问题 10 来自 Project Euler就是在给定的 n 下找到所有素数的总和。

我简单地通过总结埃拉托色尼筛法生成的素数就解决了这个问题。然后我遇到了更有效率的solution by Lucy_Hedgehog (亚线性!)。

对于 n = 2⋅10^9:

  • Python code (来自上面的引述)在 Python 2.7.3 中运行时间为 1.2 秒。

  • C++ code (我的)运行时间约为 0.3 秒(使用 g++ 4.8.4 编译)。

我在 Haskell 中重新实现了相同的算法,因为我正在学习它:

import Data.List

import Data.Map (Map, (!))
import qualified Data.Map as Map

problem10 :: Integer -> Integer
problem10 n = (sieve (Map.fromList [(i, i * (i + 1) `div` 2 - 1) | i <- vs]) 2 r vs) ! n
              where vs = [n `div` i | i <- [1..r]] ++ reverse [1..n `div` r - 1]
                    r  = floor (sqrt (fromIntegral n))

sieve :: Map Integer Integer -> Integer -> Integer -> [Integer] -> Map Integer Integer
sieve m p r vs | p > r     = m
               | otherwise = sieve (if m ! p > m ! (p - 1) then update m vs p else m) (p + 1) r vs

update :: Map Integer Integer -> [Integer] -> Integer -> Map Integer Integer
update m vs p = foldl' decrease m (map (\v -> (v, sumOfSieved m v p)) (takeWhile (>= p*p) vs))

decrease :: Map Integer Integer -> (Integer, Integer) -> Map Integer Integer
decrease m (k, v) = Map.insertWith (flip (-)) k v m

sumOfSieved :: Map Integer Integer -> Integer -> Integer -> Integer
sumOfSieved m v p = p * (m ! (v `div` p) - m ! (p - 1))

main = print $ problem10 $ 2*10^9

我用 ghc -O2 10.hs 编译它并用 time ./10 运行。

它给出了正确答案,但大约需要 7 秒。

我用 ghc -prof -fprof-auto -rtsopts 10 编译它并用 ./10 +RTS -p -h 运行。

10.prof表明 decrease 需要 52.2% 的时间和 67.5% 的分配。

运行 hp2ps 10.hp 后,我得到了这样的堆配置文件:

hp

再次看起来 decrease 占用了大部分堆。 GHC 版本 7.6.3。

您将如何优化此 Haskell 代码的运行时间?


更新 13.06.17:

triedhashtables 包中的可变 Data.HashTable.IO.BasicHashTable 替换不可变的 Data.Map,但我可能做错了什么,因为对于微小的 n = 30,它已经花费了太长时间,大约 10 秒。怎么了?

更新 18.06.17:

Curious about the HashTable performance issues是一本好书。我拿了 Sherh 的 code使用可变 Data.HashTable.ST.Linear,但是 dropped Data.Judy in instead .它在 1.1 秒内运行,仍然相对较慢。

最佳答案

我做了一些小的改进,所以它在我的机器上运行 3.4-3.5 秒。 使用 IntMap.Strict 帮助很大。除此之外,我只是手动执行了一些 ghc 优化以确保安全。并通过您的链接使 Haskell 代码更接近 Python 代码。作为下一步,您可以尝试使用一些可变的 HashMap。但我不确定... IntMap 不会比某些可变容器快多少,因为它是一个不可变容器。尽管我仍然对它的效率感到惊讶。我希望这可以更快地实现。

代码如下:

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) 

更新:

使用可变的哈希表 我已经设法在 Haskell 上使用 this implementation 将性能提高到 ~5.5sec .

另外,我在几个地方使用了未装箱的向量而不是列表。 Linear 哈希似乎是最快的。我认为这可以更快地完成。我注意到 sse42 optionhasthables 包中。不确定我是否已设法正确设置它,但即使没有它也运行得那么快。

更新 2(2017 年 6 月 19 日)

通过完全删除 judy hashmap,我设法使其 3x 比 @Krom 的最佳解决方案(使用我的代码 + 他的 map )快。相反,只使用普通数组。如果您注意到 S HashMap 的键是从 1n'n 的序列,您可以想出同样的想法div i 用于 i1r。所以我们可以将这样的 HashMap 表示为两个数组,根据搜索键在数组中进行查找。

我的代码 + Judy HashMap

$ time ./judy
95673602693282040

real    0m0.590s
user    0m0.588s
sys     0m0.000s

我的代码+我的稀疏图

$ time ./sparse
95673602693282040

real    0m0.203s
user    0m0.196s
sys     0m0.004s

如果不是 IOUArray 而不是已经生成的向量和使用 Vector 库并且 readArray 替换,这可以更快地完成不安全读取。但我认为,如果您对尽可能多地优化它并不真正感兴趣,则不应该这样做。

与此解决方案进行比较是作弊,是不公平的。我希望在 Python 和 C++ 中实现相同的想法会更快。但是@Krom 封闭 HashMap 的解决方案已经在作弊,因为它使用自定义数据结构而不是标准数据结构。至少您可以看到 Haskell 中标准和最流行的 HashMap 并没有那么快。使用更好的算法和更好的临时数据结构可以更好地解决此类问题。

Here's resulting code.

关于algorithm - 如何优化这个 Haskell 代码在亚线性时间内总结素数?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/44441627/

相关文章:

代码优化,C 代码不响应缓存阻塞

r - 确定向量中是否存在元素的最有效方法

algorithm - wso2 中的 XACML 自定义 RuleCombining 算法

java - 检查 HashSet 是否包含特定子集

multithreading - 并发(多线程)程序挂起,除非我打印出一些东西

sqlite - 没有 Database.SQLite.Simple.FromField.FromField Char 的实例

c - 如何设置重复数据,使大部分可以优化掉?

c++ - 通过提供数据对象和中心点之间的距离来实现 k-medoids 算法

python - 对两个列表的操作

haskell - 带有反应香蕉和 gtk2hs 的 react 表