我有一个程序,它遍历一个执行概率分布代数的表达式树,可以进行采样或计算结果分布。
我有两个实现方法来计算分布:一个(`computeDistribution`)可以很好地重复使用monad transformer,另一个(`simpleDistribution`)需要手动具体化。我不想手动具体化所有内容,因为这会导致采样和计算代码之间存在代码复制。
我还有两个数据表示:
type Measure a = [(a, Rational)]
-- data Distribution a = Distribution (Measure a) deriving Show
newtype Distribution a = Distribution (Measure a) deriving Show
当我在可重用代码中使用
data
版本来计算 20d2 的分布时(ghc -O3 program.hs; time ./program 20 > /dev/null
),需要大约1秒钟的时间,这似乎太长了。如果选择更高的 n
值,那就要冒险了。当我使用手动具体化的代码,或者使用
newtype
表示与任一实现一起,计算 20d2 (time ./program 20 s > /dev/null
) 就会很快。为什么?
如何找出原因?
我对 Haskell 执行的了解几乎为零。我知道基本上有一个懒加载图形,与程序基本相同的形状,但这就是我所知道的全部。
我认为使用
newtype
,Distribution
的表示方式与 Measure
相同,即它只是一个列表,而使用 data
版本时,每个 Distribution
有点像一个单字段记录,除了指向包含的列表的指针外,因此 data
版本必须执行更多的分配。这是真的吗?如果是真的,这足以解释性能差异吗?我刚开始使用单子转换器栈。考虑到
simpleDistribution
中的 Let
和 Uniform
的情况,它们是否与基于 walkTree
的实现相同?我怎么判断呢?这是我的程序。请注意,
Uniform n
对应于掷一个 n 面骰子(如果一元性质令人惊讶,则忽略)。
更新: 基于评论,我简化了我的程序,删除了所有不会对性能差距产生贡献的东西。我进行了两个语义上的更改:概率现在被非规范化,并且都是错的,简化步骤消失了。但是我的程序的基本形状仍然存在。(查看问题编辑历史记录以获取非简化程序)。
更新 2: 我进一步简化了,将 Distribution
减少到列表单子并加入一点小调整,移除了所有与概率有关的内容,并缩短了名称。当使用 data
而不是 newtype
时,我仍然观察到很大的性能差异。import Control.Monad (liftM2)
import Control.Monad.Trans (lift)
import Control.Monad.Reader (ReaderT, runReaderT)
import System.Environment (getArgs)
import Text.Read (readMaybe)
main = do
args <- getArgs
let dieCount = case map readMaybe args of Just n : _ -> n; _ -> 10
let f = if ["s"] == (take 1 $ drop 1 $ args) then fast else slow
print $ f dieCount
fast, slow :: Int -> P Integer
fast n = walkTree n
slow n = walkTree n `runReaderT` ()
walkTree 0 = uniform
walkTree n = liftM2 (+) (walkTree 0) (walkTree $ n - 1)
data P a = P [a] deriving Show
-- newtype P a = P [a] deriving Show
class Monad m => MonadP m where uniform :: m Integer
instance MonadP P where uniform = P [1, 1]
instance MonadP p => MonadP (ReaderT env p) where uniform = lift uniform
instance Functor P where fmap f (P pxs) = P $ fmap f pxs
instance Applicative P where
pure x = P [x]
(P pfs) <*> (P pxs) = P $ pfs <*> pxs
instance Monad P where
(P pxs) >>= f = P $ do
x <- pxs
case f x of P fxs -> fxs
test_1 = if expensiveTest then K value1 else K value2
和test_2 = K (if expensiveTest then value1 else value2)
。如果我们执行case test_i of K x -> ...
,那么当i=2或者i=1且K
是一个newtype构造器时,expensiveTest
不会立即计算。当i=1且K
是一个数据构造器时,expensiveTest
会立即计算。对于newtypes,构造和模式匹配始终是零成本的no-ops,与data
不同。 - chicase K (error "this was evaluated") of K x -> "not using x"
:由于x
未被评估,因此不会打印错误,无论是新类型还是数据类型。相反,尝试case error "this might be evaluated" of K x -> "not using x"
:这对于数据类型会出错,但对于新类型则不会。新类型始终是无操作的,而匹配数据构造函数会评估表达式。我不知道你的程序,它太复杂了,无法立即发现问题,但这就是通常影响性能的数据类型和新类型之间的区别。 - chi