fgl
是用于图形操作的Haskell库。这个库带有一个基类的实现 - Data.Graph.Inductive.PatriciaTree
- 据说可以高度调整性能。性能调优的一部分涉及ghc RULES编译指示,用更快的专用版本替换某些通用函数。
然而,我的证据是这些规则似乎根本不起作用,我不明白为什么不这样做。对于试图完全复制我看到的内容的人,我已将我的测试项目放在https://github.com/fizbin/GraphOptiTest并使用ghc版本7.10.2
。
这是我的测试程序:
{-# LANGUAGE TupleSections #-}
module Main where
import Control.Exception
import Control.Monad
import Data.Graph.Inductive
import qualified Data.Graph.Inductive.PatriciaTree as Pt
import qualified MyPatriciaTree as MPt
makeGraph :: (DynGraph gr) => Int -> gr () Int
makeGraph n = mkGraph (map (,()) [1 .. n])
(concatMap (\x -> map (\y -> (x, y, x*y)) [x .. n]) [1 .. n])
main1 :: IO ()
main1 =
replicateM_ 200 $ let x = makeGraph 200 :: Pt.Gr () Int
in evaluate (length $ show x)
main2 :: IO ()
main2 =
replicateM_ 200 $ let x = makeGraph 200 :: MPt.Gr () Int
in evaluate (length $ show x)
main :: IO ()
main = main1 >> main2
现在,Data.Graph.Inductive.PatriciaTree
具有类函数mkGraph
的定义:
mkGraph vs es = insEdges es
. Gr
. IM.fromList
. map (second (\l -> (IM.empty,l,IM.empty)))
$ vs
其中insEdges
是模块Data.Graph.Inductive.Graph
中定义的函数:
insEdges :: (DynGraph gr) => [LEdge b] -> gr a b -> gr a b
insEdges es g = foldl' (flip insEdge) g es
而且Data.Graph.Inductive.PatriciaTree
对此有insEdge
:
{-# RULES
"insEdge/Data.Graph.Inductive.PatriciaTree" insEdge = fastInsEdge
#-}
fastInsEdge :: LEdge b -> Gr a b -> Gr a b
fastInsEdge (v, w, l) (Gr g) = g2 `seq` Gr g2
where
g1 = IM.adjust addSucc' v g
g2 = IM.adjust addPred' w g1
addSucc' (ps, l', ss) = (ps, l', IM.insertWith addLists w [l] ss)
addPred' (ps, l', ss) = (IM.insertWith addLists v [l] ps, l', ss)
所以,理论上,当我在测试程序中运行main1
时,我应该将其编译成最终调用fastInsEdge
的内容。
为了对此进行测试,我与Data.Graph.Inductive.PatriciaTree
的修改版本进行了比较,后者使用此作为mkGraph
方法的定义:(这是上面{{1}中使用的类MyPatriciaTree
}})
main2
当我运行我的测试程序( mkGraph vs es = doInsEdges
. Gr
. IM.fromList
. map (second (\l -> (IM.empty,l,IM.empty)))
$ vs
where
doInsEdges g = foldl' (flip fastInsEdge) g es
和cabal configure --enable-library-profiling --enable-executable-profiling
之后)时,cabal build GraphOptiTest
方法会抽取main2
方法。它甚至没有关闭 - 该配置文件显示该计划的99.2%的时间花在main1
内。 (并将程序更改为只运行main1
表明是的,main2
本身真的很快)
是的,我的cabal文件的main2
部分中有-O
。
尝试像ghc-options
这样的ghc选项并没有什么帮助 - 我只能看到这些替换规则没有解决,但我不明白为什么。我不知道如何让编译器告诉我为什么它没有激活替换规则。
通过弄乱-ddump-rule-firings
的来源,发现一些被发现的东西,以回应@dfeuer的答案:
如果我将fgl
的专用版本添加到insEdges
:
Data.Graph.Inductive.PatriciaTree
然后{-# RULES
"insEdges/Data.Graph.Inductive.PatriciaTree" insEdges = fastInsEdges
#-}
fastInsEdges :: [LEdge b] -> Gr a b -> Gr a b
fastInsEdges es g = foldl' (flip fastInsEdge) g es
和main1
现在都很快。此替换规则触发;为什么不是另一个? (不,告诉ghc main2
函数NOINLINE
没有好处)
EPILOGUE:
因此,现在存在一个与insEdge
包一起提交的错误,该错误未标记其使用fgl
和insEdge
的函数,以便使用快速版本。但是在我的代码中我现在解决这个问题,并且在更多情况下解决方法可能会有用,所以我想我会分享它。在我的代码的顶部,我有:
insNode
(如果我在我的代码中使用了import qualified Data.Graph.Inductive as G
import qualified Data.Graph.Inductive.PatriciaTree as Pt
-- Work around design and implementation performance issues
-- in the Data.Graph.Inductive package.
-- Specifically, the tuned versions of insNode, insEdge, gmap, nmap, and emap
-- for PatriciaTree graphs are exposed only through RULES pragmas, meaning
-- that you only get them when the compiler can specialize the function
-- to that specific instance of G.DynGraph. Therefore, I create my own
-- type class with the functions that have specialized versions and use that
-- type class here; the compiler then can do the specialized RULES
-- replacement on the Pt.Gr instance of my class.
class (G.DynGraph gr) => MyDynGraph gr where
mkGraph :: [G.LNode a] -> [G.LEdge b] -> gr a b
insNodes :: [G.LNode a] -> gr a b -> gr a b
insEdges :: [G.LEdge b] -> gr a b -> gr a b
insNode :: G.LNode a -> gr a b -> gr a b
insEdge :: G.LEdge b -> gr a b -> gr a b
gmap :: (G.Context a b -> G.Context c d) -> gr a b -> gr c d
nmap :: (a -> c) -> gr a b -> gr c b
emap :: (b -> c) -> gr a b -> gr a c
instance MyDynGraph Pt.Gr where
mkGraph nodes edges = insEdges edges $ G.mkGraph nodes []
insNodes vs g = foldl' (flip G.insNode) g vs
insEdges es g = foldl' (flip G.insEdge) g es
insNode = G.insNode
insEdge = G.insEdge
gmap = G.gmap
nmap = G.nmap
emap = G.emap
函数,我也会将它包含在类中)然后,我以前用nemap
编写的任何代码现在用术语编写(G.DynGraph gr) => ...
。编译器RULES激活(MyDynGraph gr) => ...
实例,然后我获得每个函数的优化版本。
基本上,这会削弱编译器将任何这些函数内联到调用代码中的能力,并可能进行其他优化以始终获得优化版本。 (以及在运行时额外指针间接的成本,但相比之下这是微不足道的)因为分析表明那些其他优化无论如何都没有产生任何重要意义,这在我的案例中是一个明显的净胜利。
许多人的代码可以积极地使用Pt.Gr
规则来获得各地的优化版本;但是,有时这是不可能的,并且如果没有重构应用程序的大块,那么实际的生产代码就不会导致我的问题。我有一个数据结构,其成员类型为SPECIALIZE
- 现在使用(forall gr. G.DynGraph gr => tokType -> gr () (MyEdge c))
作为类约束,但完全展开它以使签名中没有MyDynGraph
将是巨大的努力,这样的签名阻止专业化跨越边界。
答案 0 :(得分:1)
我还没有做过任何实验,但这是我的猜测。 insEdge
函数未标记为(已定相)INLINE
或NOINLINE
,因此只要内核完全应用,内联器就可以自由内联。在insEdges
的定义中,我们看到了
foldl' (flip insEdge) g es
内联foldl'
给出了
foldr f' id es g
where f' x k z = k $! flip insEdge z x
flip
现已完全应用,因此我们可以内联它:
foldr f' id es g
where f' x k z = k $! insEdge x z
现在insEdge
已完全应用,因此GHC可能会选择在规则有机会之前在那里内联它。
尝试按{-# NOINLINE [0] insEdge #-}
的定义添加insEdge
,看看会发生什么。如果有效,请向fgl
提交拉取请求。
P.S。在我看来,这种事情应该通过使用默认的类方法来完成,而不是重写规则。规则总是有点挑剔。
正如评论所揭示的那样,最大的问题不是过早的内联,而是未能专攻insEdge
。特别是,Data.Graph.Inductive.Graph
不会导出insEdges
的展开,因此无法将其专门化,并且它调用insEdge
到适当的类型。最终的解决方案是标记insEdges
INLINABLE
,但我仍然建议您在谨慎的情况下标记insEdge
NOINLINE [0]
。