例如,假设我要在列表上编写一个单子(monad)图和非单子(monad)图。我将从一元论开始:

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(而不是重复代码):
map' :: (a -> b) -> ([a] -> [b])
map' f = runIdentity . mapM' (Identity . f)

有什么必要使map'written explicitly like map is一样被优化? 特别是:
  • 是否有必要写
    {-# SPECIALIZE mapM' :: (a -> Identity b) -> ([a] -> Identity [b]) #-}
    

    还是GHC会优化map'本身(通过完全排除Identity)?
  • 还需要添加其他内容(更多实用指示)吗?
  • 如何通过map'的显式编写代码来验证已编译map的优化程度如何?
  • 最佳答案

    好吧,让我们问一下编译器本身。
    编译模块

    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)
    
    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]))
        }
    
    是的,只有cast,没有实际开销。您会得到一个本地工作的go,其作用与map完全相同。
    总结:您只需要-O2,并且可以通过查看内核(-ddump-simpl)或在生成的程序集(-ddump-asm)或LLVM位代码-ddump-llvm(如果可以阅读)中验证代码的优化程度。
    详细说明可能是一件好事。关于

    有必要写吗
    {-# SPECIALIZE mapM' :: (a -> Identity b) -> ([a] -> Identity [b]) #-}
    
    还是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
    
    这也有利于在定义模块之外的mapM' monad上使用Identity(如果进行了优化编译,并且该monad及时被识别为Identity以便触发规则)。
    但是,如果GHC不了解可以专精的类型,那么它可能看不到任何好处,也无法专精(我不知道它是否会尝试使用-到目前为止,我已经找到了每种专精时间我看)。
    如果您想确定,请查看核心。
    如果您需要在其他模块中进行专业化处理,则GHC在编译定义模块时没有理由对功能进行专业化处理,因此在这种情况下,必须进行编译。从ghc-7开始,最好使用{-# SPECIALISE #-}编译指示,而不是{-# INLINABLE #-}编译指示要求对一些手动选择的类型进行专业化处理,以便更好地使用{#- INLINABLE #-}编译指示,以便在导入模块中可以访问(略作修改的)源代码,允许在那里进行任何所需类型的专业化。

    还有什么需要补充的吗?

    当然,不同的用法可能需要不同的编译指示,但根据经验,{-# RULES #-}是您最想要的。当然map'可以实现编译器无法自行完成的功能。

    如何验证为map明确编写的代码,对编译后的ojit_code的优化程度如何?
  • 查看所产生的内核,asm或llvm位代码,以您最了解的为准(内核相对容易)。
  • 如果您不确定核心知识并且需要知道的话,可以将生成的代码与手写专业标准进行基准比较。最终,除非您在某个阶段(core / cmm / asm / llvm)获得相同的中间结果,否则基准测试是唯一可以肯定的方法。
  • 10-06 13:29
    查看更多