【发布时间】:2018-06-25 10:39:00
【问题描述】:
我已经编写了这个计算 Collatz 序列的函数,并且我看到根据我给它的自旋而不同的执行时间。显然它与所谓的“记忆化”有关,但我很难理解它是什么以及它是如何工作的,不幸的是,HaskellWiki 上的相关文章以及它链接到的论文都被证明不是很容易克服。他们讨论了高度外行不可区分的树结构的相对性能的复杂细节,而我错过的一定是这些来源忽略提及的一些非常基本、非常琐碎的点。
这是代码。这是一个完整的程序,可以构建和执行。
module Main where
import Data.Function
import Data.List (maximumBy)
size :: (Integral a) => a
size = 10 ^ 6
-- Nail the basics.
collatz :: Integral a => a -> a
collatz n | even n = n `div` 2
| otherwise = n * 3 + 1
recollatz :: Integral a => a -> a
recollatz = fix $ \f x -> if (x /= 1)
then f (collatz x)
else x
-- Now, I want to do the counting with a tuple monad.
mocollatz :: Integral b => b -> ([b], b)
mocollatz n = ([n], collatz n)
remocollatz :: Integral a => a -> ([a], a)
remocollatz = fix $ \f x -> if x /= 1
then f =<< mocollatz x
else return x
-- Trivialities.
collatzLength :: Integral a => a -> Int
collatzLength x = (length . fst $ (remocollatz x)) + 1
collatzPairs :: Integral a => a -> [(a, Int)]
collatzPairs n = zip [1..n] (collatzLength <$> [1..n])
longestCollatz :: Integral a => a -> (a, Int)
longestCollatz n = maximumBy order $ collatzPairs n
where
order :: Ord b => (a, b) -> (a, b) -> Ordering
order x y = snd x `compare` snd y
main :: IO ()
main = print $ longestCollatz size
使用 ghc -O2 大约需要 17 秒,如果没有 ghc -O2 - 大约需要 22 秒来传递从 size 下方的任意点开始的最长 Collatz 序列的长度和种子。
现在,如果我进行这些更改:
diff --git a/Main.hs b/Main.hs
index c78ad95..9607fe0 100644
--- a/Main.hs
+++ b/Main.hs
@@ -1,6 +1,7 @@
module Main where
import Data.Function
+import qualified Data.Map.Lazy as M
import Data.List (maximumBy)
size :: (Integral a) => a
@@ -22,10 +23,15 @@ recollatz = fix $ \f x -> if (x /= 1)
mocollatz :: Integral b => b -> ([b], b)
mocollatz n = ([n], collatz n)
-remocollatz :: Integral a => a -> ([a], a)
-remocollatz = fix $ \f x -> if x /= 1
- then f =<< mocollatz x
- else return x
+remocollatz :: (Num a, Integral b) => b -> ([b], a)
+remocollatz 1 = return 1
+remocollatz x = case M.lookup x (table mutate) of
+ Nothing -> mutate x
+ Just y -> y
+ where mutate x = remocollatz =<< mocollatz x
+
+table :: (Ord a, Integral a) => (a -> b) -> M.Map a b
+table f = M.fromList [ (x, f x) | x <- [1..size] ]
-- Trivialities.
-- 然后使用ghc -O2 只需要大约 4 秒,但如果没有 ghc -O2,我不会活到看到它完成的时间。
查看带有ghc -prof -fprof-auto -O2 的成本中心的详细信息,发现第一个版本输入collatz 大约一亿次,而修补过的版本——大约一50 万次。这一定是加速的原因,但我很难理解这个魔法的内部运作。我最好的想法是我们用 O(log n) 映射查找替换一部分昂贵的递归调用,但我不知道它是否正确以及为什么它如此依赖于一些被上帝遗忘的编译器标志,而在我看来,这种性能波动应该完全来自语言。
我能否解释一下这里发生了什么,以及为什么ghc -O2 和普通ghc 构建之间的性能差异如此之大?
附: Stack Overflow 上其他地方强调了实现自动记忆的两个要求:
将要记忆的函数设为顶级名称。
将要记忆的函数设为单态函数。
根据这些要求,我重构remocollatz如下:
remocollatz :: Int -> ([Int], Int)
remocollatz 1 = return 1
remocollatz x = mutate x
mutate :: Int -> ([Int], Int)
mutate x = remocollatz =<< mocollatz x
现在它已经达到了最高水平和单态性。运行时间约为 11 秒,与类似的单态 table 版本相比:
remocollatz :: Int -> ([Int], Int)
remocollatz 1 = return 1
remocollatz x = case M.lookup x (table mutate) of
Nothing -> mutate x
Just y -> y
mutate :: Int -> ([Int], Int)
mutate = \x -> remocollatz =<< mocollatz x
table :: (Int -> ([Int], Int)) -> M.Map Int ([Int], Int)
table f = M.fromList [ (x, f x) | x <- [1..size] ]
-- 运行不到 4 秒。
我想知道为什么 memoization ghc 在第一种情况下的执行速度几乎比我的哑桌子慢 3 倍。
【问题讨论】:
-
如果这篇文章很长而且写得不好,我很抱歉。我会努力打造自己的风格。
-
看来您的问题不仅在于理解 Haskell 或优化的重要性,还在于 memoization 的概念(更广泛的编程概念),对吗?您是否查看过通用 CS 或 wikipedia/books 资源以获取记忆解释?
-
我问了一个关于 Haskell 中 memoization 的问题,也许这个答案也可以给你一些指导。 stackoverflow.com/questions/11473130/…
-
-
@Kindaro 您将很多内容都包含在一个问题中,专注于一个问题而不是整个蠕虫罐头。优化很重要。时期。我会单独问这个。 Memoized fibs 很容易,因为您始终知道输入
n将是比当前呼叫少的每个数字。 Memoized collatz 使用映射(通常)而不是列表,因为对 collatz 的迭代调用不仅仅是n-1和n-2,而是一种相当不可预测的形式。最后,不要想,甚至不要说,fibs memoization 是魔法。如果操作不清楚,请先了解。
标签: haskell memoization