如果我将一个函数从它的使用位置移动到一个单独的模块中,我注意到该程序的性能显着下降。
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的最低限度。
答案 0 :(得分:8)
对于类型类多态fromDigits
,由于(+)
,(*)
和fromInteger
的字典查找,您得到的函数太大而无法使用它的展开自动曝光。这意味着它无法专门用于调用站点,并且无法将字典查找消除为可能的内联加法和乘法(这可能会进一步优化)。
当它与在其中使用的模块中定义时,通过优化,GHC为其使用的类型创建专用版本(如果已知)。然后可以消除字典查找,并且可以内联(+)
和(*)
操作(如果它们使用的类型具有适合于内联的操作)。
但这取决于所知的类型。因此,如果您在一个模块中具有多态calc
和fromDigits
,但仅在其他模块中使用它,那么您再次处于只有通用版本可用的位置,但由于它的展开不是暴露,它不能在呼叫站点专门或以其他方式优化。
一种解决方案是在接口文件中展开函数的展开,因此当必要的数据(特别是类型)可用时,可以在使用它的地方对其进行适当优化。您可以通过向函数添加{-# INLINE #-}
,或者从GHC 7添加{-# INLINABLE #-}
pragma,在接口文件中公开函数的展开。这使得在编译调用代码时几乎不变的源代码可用,因此可以使用更多可用信息正确地优化该函数。
这样做的缺点是代码膨胀,你得到每个调用站点的优化代码的副本(对于INLINABLE
它不是那么极端,每个调用模块至少有一个副本,这通常不会太多坏)。
另一种解决方案是通过添加{-# SPECIALISE #-}
pragma(也接受美国拼写)在定义模块中生成专用版本,让GHC为重要类型(Int
,{{1}创建优化版本},Integer
,?)。这也创建了重写规则,因此在专门的for类型中的使用被重写以使用专用版本(当使用优化进行编译时)。
这样做的缺点是在内联代码时可能会有一些优化。
答案 1 :(得分:5)
您可以使用inline
函数告诉GHC在呼叫站点内联函数:http://www.haskell.org/ghc/docs/7.0.4/html/libraries/ghc-prim-0.2.0.0/GHC-Prim.html#v%3Ainline。您可能希望将其与INLINABLE
pragma:http://www.haskell.org/ghc/docs/7.0.4/html/users_guide/pragmas.html#inlinable-pragma