同构中的森林砍伐

Deforestation in a Hylomorphism

维基百科关于 Hylomorphism 的描述:

In [...] functional programming a hylomorphism is a recursive function, corresponding to the composition of an anamorphism (which first builds a set of results; also known as 'unfolding') followed by a catamorphism (which then folds these results into a final return value). Fusion of these two recursive computations into a single recursive pattern then avoids building the intermediate data structure. This is an example of deforestation, a program optimization strategy.

(我加粗的标记)

使用 recursion-schemes 库 我写了一个很简单的同态:

import Data.Functor.Foldable
main :: IO ()
main = putStrLn $ show $ hylosum 1000

hylosum :: Int -> Int
hylosum end = hylo alg coalg 1
  where 
    -- Create list of Int's from 1 to n
    coalg :: Int -> ListF Int Int
    coalg n 
       | n > end = Nil
       | otherwise = Cons n (n + 1)
    -- Sum up a list of Int's
    alg :: ListF Int Int -> Int
    alg Nil  =  0
    alg (Cons a x) = a + x

在cabal文件中我指示GHC优化代码:

name:                Hylo
version:             0.1.0.0
synopsis:            Hylomorphisms and Deforestation        
build-type:          Simple
cabal-version:       >=1.10

executable             Hylo
  main-is:             Main.hs
  ghc-options:         -O2
  build-depends:       base >=4.10 && <4.11 , recursion-schemes      
  default-language:    Haskell2010

使用 stackage lts-10.0 (GHC 8.2.2) 我用 stack build 和 运行 用 stack exec Hylo -- +RTS -s 编译,我得到:

500500
      84,016 bytes allocated in the heap
       3,408 bytes copied during GC
      44,504 bytes maximum residency (1 sample(s))
      25,128 bytes maximum slop
           2 MB total memory in use (0 MB lost due to fragmentation)

现在我将 hylosum 1000 更改为 hylosum 1000000(增加 1000 倍),我得到:

500000500000
  16,664,864 bytes allocated in the heap
      16,928 bytes copied during GC
  15,756,232 bytes maximum residency (4 sample(s))
      29,224 bytes maximum slop
          18 MB total memory in use (0 MB lost due to fragmentation)

所以堆使用量从 84 KB 上升到 16,664 KB。这比以前多了 200 倍。 因此我认为,GHC 不会进行维基百科中提到的森林砍伐/融合!

这并不奇怪:变形从左到右创建列表项 (从 1 到 n)并且变形从右到左以相反的方式消耗项目 (从 n 到 1)并且很难看出同态是如何工作的 无需创建整个中间列表。

问题: GHC 是否能够进行森林砍伐? 如果 yes,我必须在我的代码或 cabal 文件中更改什么? 如果,它究竟是如何工作的? 如果 ,问题出在哪里:维基百科、GHC 还是图书馆?

数据结构实际上被融合掉了,但是生成的程序不是尾递归的。优化后的代码基本上是这样的(看不到 ConsNil):

h n | n > end = 0
    | otherwise = n + h (n+1)

要计算结果,您必须首先递归地计算 h (n+1),然后将结果添加到 n。在递归调用期间,值 n 必须保留在某处,因此我们观察到随着 end 增加,内存使用量增加。

通过将递归调用置于尾部位置并携带一个恒定大小的累加器,可以获得更紧密的循环。我们希望代码对此进行优化:

-- with BangPatterns
h n !acc | n > end = acc
         | otherwise = h (n+1) (n + acc)

hylosum 中,对 (+) 的调用发生在 alg 中,我们将其替换为对将由 hylo 构建的延续的调用。

alg :: ListF Int (Int -> Int) -> Int -> Int
alg Nil acc = acc
alg (Cons n go) !acc = go (n + acc)

由此我看到在堆中分配了一个常量 51kB。