Haskell 为什么在这个例子中StateT更快?
我试图对非一元a->a函数、StateT和IORef之间更新字段的性能差异进行基准测试。我的基准代码如下:Haskell 为什么在这个例子中StateT更快?,haskell,Haskell,我试图对非一元a->a函数、StateT和IORef之间更新字段的性能差异进行基准测试。我的基准代码如下: {-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE MultiParamTypeClasses #-} {-# LANGUAGE GeneralizedNewtypeDeriving #-} {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE BangPatterns #-} module Main w
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE BangPatterns #-}
module Main where
import Control.Monad.State.Strict
import Criterion.Main
import Data.IORef
import Data.List
newtype MyStateT s m a = MyStateT { unMyStateT :: StateT s m a }
deriving (Functor, Applicative, Monad, MonadState s)
runMyStateT = runStateT . unMyStateT
data Record = Record
{ ra :: Int
, rb :: String
, rc :: Int
, rd :: Int
} deriving (Show)
newRecord :: IO (IORef Record)
newRecord = newIORef Record
{ ra = 0
, rb = "string"
, rc = 20
, rd = 30
}
updateRecordPure :: Record -> Record
updateRecordPure !r = r { ra = ra r + 1 }
updateRecord :: IORef Record -> IO ()
updateRecord ref = do
r <- readIORef ref
writeIORef ref $ r { ra = ra r + 1 }
modifyRecord :: IORef Record -> IO ()
modifyRecord ref = modifyIORef' ref (\r -> r { ra = ra r + 1 })
updateRecordM :: (MonadState Record m) => m ()
updateRecordM = modify' $ \r -> r { ra = ra r + 1 }
numCycles :: [Int]
numCycles = [1..10000]
runUpdateRecordPure :: Record -> Record
runUpdateRecordPure rec = foldl' update rec numCycles
where
update !r _ = updateRecordPure r
runUpdateRecord :: IO ()
runUpdateRecord = do
r <- newRecord
mapM_ (\_ -> updateRecord r) numCycles
runModifyRecord :: IO ()
runModifyRecord = do
r <- newRecord
mapM_ (\_ -> modifyRecord r) numCycles
runModifyRecordStateM :: (MonadState Record m) => m ()
runModifyRecordStateM = mapM_ (const updateRecordM) numCycles
main = defaultMain
[ bgroup "Pure"
[ bench "update" $ whnf runUpdateRecordPure rec
]
, bgroup "IORef record"
[ bench "update" $ whnfIO runUpdateRecord
, bench "modify" $ whnfIO runModifyRecord
]
, bgroup "MyStateT"
[ bench "modify" $ whnfIO (snd <$> runMyStateT runModifyRecordStateM rec)
]
]
where
rec = Record
{ ra = 0
, rb = "string"
, rc = 20
, rd = 30
}
从结果来看,StateT版本的速度几乎是非一元版本的四倍,比IORef版本快两倍
代码是使用-O2、-threaded和-fno full laziness编译的(添加-fno full laziness后,结果变化不大)。我试着从whnf
/whnfIO
切换到nf
/nfIO
,但唯一改变的是非一元版本变得更慢
有人能解释一下为什么本例中的StateT版本比其他版本的性能好得多吗?除了“更新变量的速度有多快”之外,该基准测试还测试了很多东西。这里的主要问题是哈斯克尔的懒惰。像
updateRecordPure
这样简单的东西并没有达到预期的效果:
updateRecordPure :: Record -> Record
updateRecordPure !r = r { ra = ra r + 1 }
它强制r
到弱头范式,当然。但是,ra
字段没有计算,我们可以很容易地证明这一点:
-- This just evaluates to (), it doesn't diverge.
updateRecordPure Record {} `seq` ()
所以这里发生的事情是,updateRecordPure
正在创建一个记录
,其中包含一个thunk。一般来说,这个问题(累积thunks)是优化Haskell程序的常见问题,其他基准测试也会遇到这个问题
有一个简单的实验,我们可以运行,看看除了增加一个变量之外,是否还有其他事情发生。所有这些更新都应该占用固定的时间和空间,除非它们在内存中积累了大量的数据。尝试将10000调整到100000…您会发现运行时间增加了10倍以上
我已经在一个应用程序中对基准测试进行了修改和清理,它将迭代次数作为命令行参数。它还做了一些其他更改,如删除列表和使用replicateM\uuu
,这有点惯用。在我的系统上,从10000到100000次迭代具有以下效果:
- Pure/update需要80倍的时间
- IORef记录/更新需要30倍的时间
- IORef记录/修改所需时间为23倍
- MyStateT/identity需要1倍的时间,并且
- MyStateT/io的时间是它的13倍
MyStateT/identity
基准只是应用于identity
monad的MyStateT
。不知何故,GHC能够完全优化此案例,此案例的运行时间为14 ns。。。无论您使用多少次迭代
但是对于其他人来说,由于迭代次数增加10倍,运行时间增加了10倍以上,我们知道这里发生了一些事情,而不仅仅是增加一个整数和分配一个记录
确定基准
修复基准测试的惰性方法是使记录字段严格
data Record = Record
{ ra :: !Int
, rb :: String
, rc :: Int
, rd :: Int
} deriving (Show)
通过此更改,从10000到100000次迭代将Pure/update、IORef-record/modify和MyStateT/io的运行时间增加了约10倍。IORef记录/更新仍然像预期的那样缓慢,因为它正在堆上构建一个由10000或100000个块组成的链,然后在最后对它们进行评估(这种行为众所周知,并记录在modifyIORef
文档中,尽管它仍然让许多Haskell程序员大吃一惊)
在我贫血的VPS上,带有严格ra
字段的新版本有以下10000次迭代,从最快到最慢排列:
MyStateT/identity
基准测试仍然以某种方式触发了一些GHC优化,从而消除了循环。从其他实现来看,pure是最快的,这是预期的,并且添加额外的复杂性(使用IORef,然后使用IO+StateT)会使基准测试速度变慢。最后,readIORef
+writeIORef
是最慢的,因为它创建了大量thunk
请注意,纯实现每次迭代只需要7ns
不使用
-线程进行编译
会大大减少运行时间,使Pure/update、IORef-record/modify和MyStateT/io之间的距离不超过25%。因此我们可以得出结论,这些差异是由于在多线程程序中使用IO所需的某种同步造成的,或者可能是多线程程序的代码生成不同,这会阻止某些类型的优化优化我们的基准测试。OP已将其问题更新为使用Control.Monad.State.Strict
,但结果仍然相似。如果你更新了问题,但没有对答案发表评论,我无法知道问题已经改变了。
data Record = Record
{ ra :: !Int
, rb :: String
, rc :: Int
, rd :: Int
} deriving (Show)