如何正确使用GHC的SPECIALIZE pragma?(示例:使用Identity专门化来自monadic的纯函数.)

Pet*_*lák 16 optimization haskell ghc explicit-specialization

举个例子,假设我想在列表上写一个monadic和非monadic地图.我将从monadic开始:

import Control.Monad
import Control.Monad.Identity

mapM' :: (Monad m) => (a -> m b) -> ([a] -> m [b])
mapM' _ [] = return []
mapM' f (x:xs) = liftM2 (:) (f x) (mapM f xs)
Run Code Online (Sandbox Code Playgroud)

现在我想重用代码来编写纯map(而不是重复代码):

map' :: (a -> b) -> ([a] -> [b])
map' f = runIdentity . mapM' (Identity . f)
Run Code Online (Sandbox Code Playgroud)

什么是必要的,使map'优化的,如果它被明确地写的map特别是:

  1. 是否有必要写

    {-# SPECIALIZE mapM' :: (a -> Identity b) -> ([a] -> Identity [b]) #-}
    
    Run Code Online (Sandbox Code Playgroud)

    或者GHC是否map'自我优化(通过Identity完全分解)?

  2. 还需要添加其他(更多pragma)吗?

  3. 如何通过map'明确编写的代码验证编译的优化程度map

Dan*_*her 20

好吧,让我们问编译器本身.

编译模块

module PMap where

import Control.Monad
import Control.Monad.Identity

mapM' :: (Monad m) => (a -> m b) -> ([a] -> m [b])
mapM' _ [] = return []
mapM' f (x:xs) = liftM2 (:) (f x) (mapM f xs)

map' :: (a -> b) -> ([a] -> [b])
map' f = runIdentity . mapM' (Identity . f)
Run Code Online (Sandbox Code Playgroud)

with ghc -O2 -ddump-simpl -ddump-to-file PMap.hs(ghc-7.6.1,7.4.2产生相同的除了唯一名称)产生以下核心map'

PMap.map'
  :: forall a_afB b_afC. (a_afB -> b_afC) -> [a_afB] -> [b_afC]
[GblId,
 Arity=2,
 Caf=NoCafRefs,
 Str=DmdType LS,
 Unf=Unf{Src=<vanilla>, TopLvl=True, Arity=2, Value=True,
         ConLike=True, WorkFree=True, Expandable=True,
         Guidance=IF_ARGS [60 30] 160 40}]
PMap.map' =
  \ (@ a_c) (@ b_d) (f_afK :: a_c -> b_d) (eta_B1 :: [a_c]) ->
    case eta_B1 of _ {
      [] -> GHC.Types.[] @ b_d;
      : x_afH xs_afI ->
        GHC.Types.:
          @ b_d
          (f_afK x_afH)
          (letrec {
             go_ahZ [Occ=LoopBreaker]
               :: [a_c] -> Data.Functor.Identity.Identity [b_d]
             [LclId, Arity=1, Str=DmdType S]
             go_ahZ =
               \ (ds_ai0 :: [a_c]) ->
                 case ds_ai0 of _ {
                   [] ->
                     (GHC.Types.[] @ b_d)
                     `cast` (Sym <(Data.Functor.Identity.NTCo:Identity <[b_d]>)>
                             :: [b_d] ~# Data.Functor.Identity.Identity [b_d]);
                   : y_ai5 ys_ai6 ->
                     (GHC.Types.:
                        @ b_d
                        (f_afK y_ai5)
                        ((go_ahZ ys_ai6)
                         `cast` (<Data.Functor.Identity.NTCo:Identity <[b_d]>>
                                 :: Data.Functor.Identity.Identity [b_d] ~# [b_d])))
                     `cast` (Sym <(Data.Functor.Identity.NTCo:Identity <[b_d]>)>
                             :: [b_d] ~# Data.Functor.Identity.Identity [b_d])
                 }; } in
           (go_ahZ xs_afI)
           `cast` (<Data.Functor.Identity.NTCo:Identity <[b_d]>>
                   :: Data.Functor.Identity.Identity [b_d] ~# [b_d]))
    }
Run Code Online (Sandbox Code Playgroud)

是的,只有casts,没有真正的开销.你得到一个go完全一样的本地工人map.

总结:您只需要-O2,并且您可以通过查看core(-ddump-simpl)或者如果您可以在生成的assembly(-ddump-asm)resp LLVM位代码中读取它来验证代码的优化程度-ddump-llvm.

详细阐述可能是件好事.关于

是否有必要写

{-# SPECIALIZE mapM' :: (a -> Identity b) -> ([a] -> Identity [b]) #-}
Run Code Online (Sandbox Code Playgroud)

或GHC是否优化map'自身(完全分解身份)?

答案是,如果你在定义一般函数的同一个模块中使用特化,那么一般来说你不需要一个{-# SPECIALISE #-}编译指示,如果它看到任何好处,GHC就会自己创建特化.在上面的模块中,GHC创建了专门化规则

"SPEC PMap.mapM' [Data.Functor.Identity.Identity]" [ALWAYS]
    forall (@ a_abG)
           (@ b_abH)
           ($dMonad_sdL :: GHC.Base.Monad Data.Functor.Identity.Identity).
      PMap.mapM' @ Data.Functor.Identity.Identity
                 @ a_abG
                 @ b_abH
                 $dMonad_sdL
      = PMap.mapM'_$smapM' @ a_abG @ b_abH
Run Code Online (Sandbox Code Playgroud)

这也有利于定义模块之外mapM'Identitymonad的任何使用(如果使用优化进行编译,并且monad被识别为Identity及时触发规则).

但是,如果GHC不能很好地理解专业的类型,它可能看不到任何好处而且没有专门化(我不知道它是否足以判断它是否会尝试 - 到目前为止我已经找到了一个专业化我看的时间).

如果你想确定,看看核心.

如果您需要在不同模块中进行专门化,GHC在编译定义模块时没有理由专门使用该函数,因此在这种情况下需要一个pragma.而不是{-# SPECIALISE #-}要求对一些精选类型进行专门化的{-# INLINABLE #-}编译指示,使用编译指示可能更好 - 从ghc-7开始 - 以便在导入模块中可以访问(略微修改的)源代码,这允许特化对于任何所需的类型.

还需要添加其他(更多pragma)吗?

不同的用途当然可能需要不同的pragma,但根据经验,这{#- INLINABLE #-}是你最想要的.而且当然{-# RULES #-}可以做编译器不能自己做的魔术.

如何通过map'明确编写的代码验证编译的优化程度map

  • 查看生产的核心,asm或llvm bitcode,无论你最了解哪个(核心相对容易).
  • 如果您不确定核心,并且需要了解,则根据手写专业化对生成的代码进行基准测试.最终,除非您在某个阶段获得相同的中间结果(core/cmm/asm/llvm),否则基准测试是唯一可以确定的方法.