Haskell 内联派生类型类方法

Haskell 内联派生类型类方法,haskell,Haskell,Haskell允许您派生类型类实例,例如: {-# LANGUAGE DeriveFunctor #-} data Foo a = MakeFoo a a deriving (Functor) 。。。但有时基准测试表明,如果手动实现typeclass实例并使用内联注释type类方法,性能会得到提高,如下所示: data Foo a = MakeFoo a a instance Functor Foo where fmap f (MakeFoo x y) = MakeFoo (f x

Haskell允许您派生类型类实例,例如:

{-# LANGUAGE DeriveFunctor #-}

data Foo a = MakeFoo a a deriving (Functor)
。。。但有时基准测试表明,如果手动实现typeclass实例并使用
内联
注释type类方法,性能会得到提高,如下所示:

data Foo a = MakeFoo a a

instance Functor Foo where
    fmap f (MakeFoo x y) = MakeFoo (f x) (f y)
    {-# INLINE fmap #-}
有什么办法可以两全其美?换句话说,是否有一种方法可以派生typeclass实例,并使用
内联
注释派生的typeclass方法?

尽管不能像使用动态语言中的类那样在Haskell中“重新打开”实例,有一些方法可以通过向GHC传递某些标志来确保函数尽可能地进行积极的内联

-fsspecialise积极地
消除了关于哪些函数是专门化的限制。任何重载函数都将 专门研究这面旗帜。这可能会产生大量的错误 附加代码

-fexpose所有展开
将包括接口文件中所有函数的(优化)展开,以便它们可以内联和 跨模块专业化

同时使用这两个标志将产生几乎相同的效果 将每个定义标记为
可内联
,除了
内置定义的展开未得到优化

(来源:)

这些选项将允许GHC编译器内联
fmap
。尤其是
-feexpose all unfolings
选项,允许编译器将
Data.Functor
的内部内容公开给程序的其余部分,以用于内联(并且它似乎提供了最大的性能优势)。下面是我编写的一个快速而愚蠢的基准测试:

functor.hs
包含以下代码:

{-# LANGUAGE DeriveFunctor #-}
{-# LANGUAGE Strict #-}

data Foo a = MakeFoo a a deriving (Functor)

one_fmap foo = fmap (+1) foo

main = sequence (fmap (\n -> return $ one_fmap $ MakeFoo n n) [1..10000000])
编译时没有参数:

$ time ./functor 

real    0m4.036s
user    0m3.550s
sys 0m0.485s
使用
-fexpose所有展开进行编译

$ time ./functor

real    0m3.662s
user    0m3.258s
sys 0m0.404s
下面是此编译中的
.prof
文件,以显示对
fmap
的调用确实正在内联:

    Sun Oct  7 00:06 2018 Time and Allocation Profiling Report  (Final)

       functor +RTS -p -RTS

    total time  =        1.95 secs   (1952 ticks @ 1000 us, 1 processor)
    total alloc = 4,240,039,224 bytes  (excludes profiling overheads)

COST CENTRE MODULE SRC              %time %alloc

CAF         Main   <entire-module>  100.0  100.0


                                                                     individual      inherited
COST CENTRE MODULE                SRC             no.     entries  %time %alloc   %time %alloc

MAIN        MAIN                  <built-in>       44          0    0.0    0.0   100.0  100.0
 CAF        Main                  <entire-module>  87          0  100.0  100.0   100.0  100.0
 CAF        GHC.IO.Handle.FD      <entire-module>  84          0    0.0    0.0     0.0    0.0
 CAF        GHC.IO.Encoding       <entire-module>  77          0    0.0    0.0     0.0    0.0
 CAF        GHC.Conc.Signal       <entire-module>  71          0    0.0    0.0     0.0    0.0
 CAF        GHC.IO.Encoding.Iconv <entire-module>  58          0    0.0    0.0     0.0    0.0
使用两个标志编译:

$ time ./functor

real    0m3.665s
user    0m3.213s
sys 0m0.452s

这些小基准绝不代表实际代码中的性能(或文件大小),但它明确表明您可以强制GHC编译器内联
fmap
(并且它确实可以对性能产生不可忽略的影响)。事实上,我想我有一个。这对于我的用例来说已经足够好了,因为如果我想限制它的范围,我可以将类型及其实例拆分成一个单独的Haskell模块,然后添加
{-#OPTIONS\u GHC-feexpose all unfolings}
{-#OPTIONS\u GHC-fspecialise}
至屏幕顶部module@GabrielGonzalez那些注释对我来说是新闻。谢谢你!另外,请注意:您仍然可以标记大型函数
NOINLINE
,以确保它们不会内联并破坏文件大小。
$ time ./functor

real    0m3.665s
user    0m3.213s
sys 0m0.452s