【问题标题】:Is it possible to make GHC optimize (deforest) generic functions such as catamorphisms?是否可以让 GHC 优化(砍伐)泛型函数,例如变态?
【发布时间】:2012-10-27 10:12:47
【问题描述】:

我真的很喜欢以通用方式处理变态/变形的想法,但在我看来它有一个显着的性能缺陷:

假设我们想要以分类方式使用树结构 - 使用通用 catamorphism function 来描述不同的折叠:

newtype Fix f = Fix { unfix :: f (Fix f) }

data TreeT r = Leaf | Tree r r
instance Functor TreeT where
    fmap f Leaf         = Leaf
    fmap f (Tree l r)   = Tree (f l) (f r)

type Tree = Fix TreeT

catam :: (Functor f) => (f a -> a) -> (Fix f -> a)
catam f = f . fmap (catam f) . unfix

现在我们可以编写如下函数:

depth1 :: Tree -> Int
depth1 = catam g
  where
    g Leaf       = 0
    g (Tree l r) = max l r

不幸的是,这种方法有一个明显的缺点:在计算过程中,TreeT Int 的新实例会在fmap 的每个级别上创建,只是为了立即被g 使用。与经典定义相比

depth2 :: Tree -> Int
depth2 (Fix Leaf) = 0
depth2 (Fix (Tree l r)) = max (depth1 l) (depth1 r)

我们的depth1 总是会变慢,给 GC 带来不必要的压力。一种解决方案是使用hylomorphisms 并将创建和折叠树结合在一起。但通常我们不想这样做,我们可能希望在一个地方创建一棵树,然后传递到其他地方以便稍后折叠。或者,以不同的变质被文件夹多次。

有没有办法让 GHC 优化depth1?像内联 catam g 然后 fusing/deforesting g . fmap ... 在里面?

【问题讨论】:

  • 我参加这个聚会迟到了,但在 Treeg(或 depth2)案例中不应该有 +1 用于计算深度的函数树的?否则,我看不到 depth1depth2 可以返回除零以外的任何值。
  • 另外,我认为depth1depth2 的定义中实际上应该是depth2

标签: optimization haskell ghc fusion catamorphism


【解决方案1】:

我相信我找到了答案。我记得读过Why does GHC make fix so confounding?,这给了我一个解决方案。

catam 的前一个定义的问题在于它是递归的,因此任何对INLINE 的尝试都会被忽略。用-ddump-simpl -ddump-to-file编译原版并读取core

Main.depth1 = Main.catam_$scatam @ GHC.Types.Int Main.depth3

Main.depth3 =
  \ (ds_dyI :: Main.TreeT GHC.Types.Int) ->
    case ds_dyI of _ {
      Main.Leaf -> Main.depth4;
      Main.Tree l_aah r_aai -> GHC.Classes.$fOrdInt_$cmax l_aah r_aai
    }

Main.depth4 = GHC.Types.I# 0

Rec {
Main.catam_$scatam =
  \ (@ a_ajB)
    (eta_B1 :: Main.TreeT a_ajB -> a_ajB)
    (eta1_X2 :: Main.Fix Main.TreeT) ->
    eta_B1
      (case eta1_X2
            `cast` (Main.NTCo:Fix <Main.TreeT>
                    :: Main.Fix Main.TreeT ~# Main.TreeT (Main.Fix Main.TreeT))
       of _ {
         Main.Leaf -> Main.Leaf @ a_ajB;
         Main.Tree l_aan r_aao ->
           Main.Tree
             @ a_ajB
             (Main.catam_$scatam @ a_ajB eta_B1 l_aan)
             (Main.catam_$scatam @ a_ajB eta_B1 r_aao)
       })
end Rec }

相比显然更糟(catam_$scatam 中的构造函数创建/消除,更多函数调用)
Main.depth2 =
  \ (w_s1Rz :: Main.Tree) ->
    case Main.$wdepth2 w_s1Rz of ww_s1RC { __DEFAULT ->
    GHC.Types.I# ww_s1RC
    }

Rec {
Main.$wdepth2 [Occ=LoopBreaker] :: Main.Tree -> GHC.Prim.Int#
[GblId, Arity=1, Caf=NoCafRefs, Str=DmdType S]
Main.$wdepth2 =
  \ (w_s1Rz :: Main.Tree) ->
    case w_s1Rz
         `cast` (Main.NTCo:Fix <Main.TreeT>
                 :: Main.Fix Main.TreeT ~# Main.TreeT (Main.Fix Main.TreeT))
    of _ {
      Main.Leaf -> 0;
      Main.Tree l_aaj r_aak ->
        case Main.$wdepth2 l_aaj of ww_s1RC { __DEFAULT ->
        case Main.$wdepth2 r_aak of ww1_X1Sh { __DEFAULT ->
        case GHC.Prim.<=# ww_s1RC ww1_X1Sh of _ {
          GHC.Types.False -> ww_s1RC;
          GHC.Types.True -> ww1_X1Sh
        }
        }
        }
    }
end Rec }

但是如果我们将catam定义为

{-# INLINE catam #-}
catam :: (Functor f) => (f a -> a) -> (Fix f -> a)
catam f = let u = f . fmap u . unfix
          in u

那么它就不再递归了,只有u里面是。这样,GHC 在depth1 的定义中内联catam 并将fmapdepth1g 融合在一起——这正是我们想要的:

Main.depth1 =
  \ (w_s1RJ :: Main.Tree) ->
    case Main.$wdepth1 w_s1RJ of ww_s1RM { __DEFAULT ->
    GHC.Types.I# ww_s1RM
    }

Rec {
Main.$wdepth1 [Occ=LoopBreaker] :: Main.Tree -> GHC.Prim.Int#
[GblId, Arity=1, Caf=NoCafRefs, Str=DmdType S]
Main.$wdepth1 =
  \ (w_s1RJ :: Main.Tree) ->
    case w_s1RJ
         `cast` (Main.NTCo:Fix <Main.TreeT>
                 :: Main.Fix Main.TreeT ~# Main.TreeT (Main.Fix Main.TreeT))
    of _ {
      Main.Leaf -> 0;
      Main.Tree l_aar r_aas ->
        case Main.$wdepth1 l_aar of ww_s1RM { __DEFAULT ->
        case Main.$wdepth1 r_aas of ww1_X1So { __DEFAULT ->
        case GHC.Prim.<=# ww_s1RM ww1_X1So of _ {
          GHC.Types.False -> ww_s1RM;
          GHC.Types.True -> ww1_X1So
        }
        }
        }
    }
end Rec }

现在与depth2 的转储相同。

【讨论】:

  • 似乎任何递归函数都可以通过将其主体移动到本地绑定来转换为非递归函数,如上面的catam。这看起来是一个有助于优化的简单步骤。我想知道为什么 GHC 不自动执行此操作。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-12-22
  • 2019-12-06
  • 2012-01-02
  • 1970-01-01
  • 1970-01-01
  • 2016-05-21
相关资源
最近更新 更多