如果函数在模块之间移动,则性能会发生巨大变化



如果我将一个函数从使用它的地方移到一个单独的模块中,我注意到程序的性能会显著下降。

calc = sum . nub . map third . filter isProd . concat . map parts . permutations
    where third (_,_,b)          = fromDigits b
          isProd (a,b,p)         = fromDigits a * fromDigits b == fromDigits p
          -- All possibilities have digits: A x AAAA or AA x AAA
          parts (a:b:c:d:e:rest) = [([a], [b,c,d,e], rest)
                                   ,([a,b], [c,d,e], rest)]

在另一个模块中:

fromDigits :: Integral a => [a] -> a                                   
fromDigits = foldl1' (a b -> 10 * a + b)

fromDigits在同一个模块中时,这将在0.1秒内运行,但当我将其移动到另一个模块时,将在0.4秒内运行。

我认为这是因为如果GHC在不同的模块中,它就不能内联函数,但我觉得应该能够内联,因为它们在同一个包中。

我不确定编译器设置是什么,但它是用Leksah/cabal默认值构建的。我敢肯定这是最低限度的-O2。

对于类型类多态fromDigits,您会得到一个函数,由于(+)(*)fromInteger的字典查找,该函数太大,无法自动公开其展开。这意味着它不能在调用站点专门化,字典查找也不能被排除在可能的内联加法和乘法之外(这可能会实现进一步的优化)。

当它被定义在与使用的模块相同的模块中时,经过优化,GHC会为它所使用的类型创建一个专门的版本(如果已知的话)。然后可以消除字典查找,并且可以内联(+)(*)操作(如果它们所使用的类型具有适合内联的操作)。

但这取决于已知的类型。因此,如果你在一个模块中有多态的calcfromDigits,但只在其他模块中使用它,那么你再次处于只有通用版本可用的位置,但由于它的展开没有公开,因此它不能在调用站点进行专门化或以其他方式优化。

一种解决方案是在接口文件中公开函数的展开,以便在必要的数据(特别是类型)可用时,在使用函数的地方对其进行适当的优化。您可以在接口文件中公开函数的展开,方法是向函数添加{-# INLINE #-},或者从GHC7开始,添加{-# INLINABLE #-}杂注。这使得在编译调用代码时可以使用几乎不变的源代码,因此可以使用更多可用信息对函数进行适当优化。

这样做的缺点是代码膨胀,你可以在每个调用站点获得一份优化的代码副本(对于INLINABLE来说,这并不极端,每个调用模块至少可以获得一份副本,这通常不会太糟)。

另一种解决方案是通过添加{-# SPECIALISE #-}杂注(也接受美国拼写)在定义模块中生成专用版本,让GHC为重要类型(IntIntegerWord,?)创建优化版本。这也创建了重写规则,因此在specialized中使用的for类型会被重写为使用specialized版本(当使用优化进行编译时)。

这样做的缺点是,当代码内联时,一些可能的优化却没有。

您可以告诉GHC使用inline函数在调用站点内联函数:http://www.haskell.org/ghc/docs/7.0.4/html/libraries/ghc-prim-0.2.0.0/GHC-Prim.html#v%3Ainline.您可能希望将它与INLINABLE杂注一起使用:http://www.haskell.org/ghc/docs/7.0.4/html/users_guide/pragmas.html#inlinable-pragma

最新更新