我怎么记忆?

时间:2018-01-16 16:43:24

标签: haskell

我编写了这个函数来计算Collat​​z序列,我看到执行的时间差别很大,这取决于我给它的旋转。显然它与“memoization”有关,但我很难理解它是什么以及它是如何工作的,不幸的是,关于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 - 从size以下的任何点开始传递长度和最长的Collat​​z序列的种子约22秒。< / p>

现在,如果我做出这些改变:

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大约一亿次,而修补后的版本大约一百五十万次。这一定是加速的原因,但我很难理解这种魔法的内在运作。我最好的想法是我们用O(log n)映射查找替换一部分昂贵的递归调用,但我不知道它是否为真,为什么它在很大程度上取决于一些被遗忘的编译器标志,而我认为,这种表演波动应该完全遵循语言。

我可以解释一下这里发生了什么,以及为什么ghc -O2和普通ghc版本之间的性能差异很大?

P.S。实现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倍。

2 个答案:

答案 0 :(得分:5)

  

我可以解释一下这里发生了什么,以及为什么ghc -O2和普通ghc构建之间的性能差别很大?

免责声明:这是猜测,未通过查看GHC核心输出进行验证。仔细回答这样做是为了验证下面概述的猜想。您可以尝试自己查看它:将-ddump-simpl添加到您的编译行,您将获得丰富的输出,详细说明GHC对您的代码所做的工作。

你写道:

remocollatz x = {- ... -} table mutate {- ... -}
  where mutate x = remocollatz =<< mocollatz x

表达式table mutate实际上并不依赖于x;但它出现在等式的右边,以x为参数。因此,在没有优化的情况下,每次调用remocollatz时都会重新计算此表(大概甚至可能来自table mutate的计算内部。)

通过优化,GHC注意到table mutate不依赖于x,并将其浮动到自己的定义,有效地产生:

fresh_variable_name = table mutate
  where mutate x = remocollatz =<< mocollatz x

remocollatz x = case M.lookup x fresh_variable_name of
    {- ... -}

因此,对于整个程序运行,该表只计算一次。

  

不知道为什么它[性能]在很大程度上取决于一些被遗忘的编译器标志,而在我看来,这样的性能波动应该完全遵循语言。

抱歉,但Haskell并不是这样的。语言定义清楚地说明给定Haskell术语的含义是什么,但没有说明计算该含义所需的运行时或内存性能。

答案 1 :(得分:2)

另一种在某些情况下工作的memoization方法,比如这个,就是使用一个盒装向量,其元素是懒惰计算的。用于初始化每个元素的函数可以在其计算中使用向量的其他元素。只要对向量元素的求值不循环并引用自身,就会评估它递归依赖的元素。一旦被评估,元素就会被有效地记忆,这样做的另一个好处就是永远不会评估从未引用的向量元素。

Collat​​z序列是这种技术的近乎理想的应用,但有一个复杂因素。从限制下的值开始的下一个Collat​​z值可能超出限制,这会在索引向量时导致范围错误。我通过迭代序列直到回到极限并计算步骤来解决这个问题。

以下程序在未优化时运行0.77秒,在优化时运行0.30秒:

import qualified Data.Vector as V

limit = 10 ^ 6 :: Int

-- The Collatz function, which given a value returns the next in the sequence.

nextCollatz val
  | odd val = 3 * val + 1
  | otherwise = val `div` 2

-- Given a value, return the next Collatz value in the sequence that is less
-- than the limit and the number of steps to get there. For example, the
-- sequence starting at 13 is: [13, 40, 20, 10, 5, 16, 8, 4, 2, 1], so if
-- limit is 100, then (nextCollatzWithinLimit 13) is (40, 1), but if limit is
-- 15, then (nextCollatzWithinLimit 13) is (10, 3).

nextCollatzWithinLimit val = (firstInRange, stepsToFirstInRange)
  where
    firstInRange = head rest
    stepsToFirstInRange = 1 + (length biggerThanLimit)
    (biggerThanLimit, rest) = span (>= limit) (tail collatzSeqStartingWithVal)
    collatzSeqStartingWithVal = iterate nextCollatz val

-- A boxed vector holding Collatz length for each index. The collatzFn used
-- to generate the value for each element refers back to other elements of
-- this vector, but since the vector elements are only evaluated as needed and
-- there aren't any loops in the Collatz sequences, the values are calculated
-- only as needed.

collatzVec :: V.Vector Int
collatzVec = V.generate limit collatzFn
  where
    collatzFn :: Int -> Int
    collatzFn index
      | index <= 1 = 1
      | otherwise = (collatzVec V.! nextWithinLimit) + stepsToGetThere
      where
        (nextWithinLimit, stepsToGetThere) = nextCollatzWithinLimit index

main :: IO ()
main = do

  -- Use a fold through the vector to find the longest Collatz sequence under
  -- the limit, and keep track of both the maximum length and the initial
  -- value of the sequence, which is the index.

  let (maxLength, maxIndex) = V.ifoldl' accMaxLen (0, 0) collatzVec
      accMaxLen acc@(accMaxLen, accMaxIndex) index currLen
        | currLen <= accMaxLen = acc
        | otherwise = (currLen, index)
  putStrLn $ "Max Collatz length below " ++ show limit ++ " is "
             ++ show maxLength ++ " at index " ++ show maxIndex