Performance Haskell:IORefs的性能
我一直在尝试用Haskell编码一个算法,该算法需要使用大量可变引用,但与纯惰性代码相比,它的速度非常慢(也许并不奇怪)。 考虑一个非常简单的例子:Performance Haskell:IORefs的性能,performance,haskell,ioref,Performance,Haskell,Ioref,我一直在尝试用Haskell编码一个算法,该算法需要使用大量可变引用,但与纯惰性代码相比,它的速度非常慢(也许并不奇怪)。 考虑一个非常简单的例子: module Main where import Data.IORef import Control.Monad import Control.Monad.Identity list :: [Int] list = [1..10^6] main1 = mapM newIORef list >>= mapM readIORef &g
module Main where
import Data.IORef
import Control.Monad
import Control.Monad.Identity
list :: [Int]
list = [1..10^6]
main1 = mapM newIORef list >>= mapM readIORef >>= print
main2 = print $ map runIdentity $ map Identity list
在我的机器上运行GHC 7.8.2时,main1
需要1.2秒,占用290MB内存,而main2
只需要0.4秒,仅占用1MB内存。有什么办法可以阻止这种增长,特别是在太空?对于与Int
不同的非基本类型,我经常需要IORef
s,并假设IORef
将使用一个与常规thunk非常类似的额外指针,但我的直觉似乎是错误的
我已经尝试过一种带有未打包的
IORef
的特殊列表类型,但没有显著差异。这很可能不是关于IORef
,而是关于严格性。IO
monad中的操作是串行的——在开始下一个操作之前,必须完成之前的所有操作。所以
mapM newIORef list
在读取任何内容之前生成一百万个IORef
s
但是,
map runIdentity . map Identity
= map (runIdentity . Identity)
= map id
它的流非常好,所以我们打印列表中的一个元素,然后生成下一个元素,以此类推
如果您希望进行更公平的比较,请使用严格的映射:
map' :: (a -> b) -> [a] -> [b]
map' f [] = []
map' f (x:xs) = (f x:) $! map' f xs
我发现解决方案的方法是使用惰性mapM
,定义为
lazyMapM :: (a -> IO b) -> [a] -> IO [b]
lazyMapM f [] = return []
lazyMapM f (x:xs) = do
y <- f x
ys <- unsafeInterleaveIO $ lazyMapM f xs
return (y:ys)
但这不起作用(您还需要使用unsafeInterleaveST
),这让我想到控件.Monad.ST.lazy
实际上有多懒。有人知道吗?:) 问题在于您使用的是mapM
,它在时间和空间上对大型列表的性能都很差。正确的解决方案是通过使用mapM
和(>=>)
融合中间列表:
它在恒定的空间内运行,性能优异,在我的机器上运行仅需0.4秒
编辑:在回答您的问题时,您还可以使用管道执行此操作,以避免手动熔断回路:
import Data.IORef
import Pipes
import qualified Pipes.Prelude as Pipes
list :: [Int]
list = [1..10^6]
main = runEffect $
each list >-> Pipes.mapM newIORef >-> Pipes.mapM readIORef >-> Pipes.print
在我的机器上,它在大约0.7秒的恒定空间内运行。谢谢。现在我研究它,我可能已经太过简化了这个问题,如果GHC真的在简化第二个映射
,也就不足为奇了。但是无论如何,像main=mapM(\r->newIORef r>>=readIORef)list>>=print这样的函数仍然需要大量的时间和空间。如果这是关于严格的话,那么我不会一个懒惰的ST
monad显著提高吗?类似于main=print$runST(mapM newSTRef list>>=mapM readSTRef)
。作为记录,使用IORef
s更慢。您的编辑应该是问题的答案,也许是控件的一个单独问题。Monad.ST.Lazy
位。ST.Lazy
并不像看上去那么懒。对状态的所有操作仍然是连续的——即,当您执行newSTRef
、readSTRef
等操作时,整个状态历史都是强制的。从某种意义上说,不依赖于国家可以懒惰地运作,这是懒惰;e、 g.foo=fmap(1:)foo将给出一个无限的列表,而不是底部。这有点令人失望,我知道……(当然,它实际上不能比这更懒惰;想想中间操作中的一个是否改变了你正在读取的变量?我们必须强迫所有的人来确定。)对不起,我已经在这上面创建了一个新的,但是现在只注意到了你的回复。这个答案可以贴在那里吗?newSTRef
和readSTRef
理论上与懒惰相处得很好writeSTRef
没有。我们必须在第一次阅读时强制所有的newSTRef
,因为我们事先不知道其中一个不是先前创建的STRef
的writeSTRef
——语义上看不到第一个mapM
中只有newSTRef
。这回答了你的问题吗?回答了。总之,我们可以安全地对newSTRef
s和readSTRef
s的序列进行分组/延迟,但是只要writeSTRef
s起作用,我们就必须重放整个历史。顺便问一下,管道库是否允许指定这种行为(而不融合这两个mapM
s)?@hpacheco是的。我用等效的pipes
解决方案更新了我的答案。实际上,我首先在管道中编写了解决方案,然后使用等式推理将其转换为等效的手写循环.Hmm,但这仍然是列表中每个元素的各种映射,就好像它是一个映射一样。例如,如何打印结果列表而不是单独打印每个元素?@hpacheco如果要打印结果列表,请使用Pipes.toListM
而不是Pipes.print
。这将把管道的输出折叠成一个列表,但比mapM
更有效。
import Data.IORef
import Control.Monad
list :: [Int]
list = [1..10^6]
main = mapM_ (newIORef >=> readIORef >=> print) list
import Data.IORef
import Pipes
import qualified Pipes.Prelude as Pipes
list :: [Int]
list = [1..10^6]
main = runEffect $
each list >-> Pipes.mapM newIORef >-> Pipes.mapM readIORef >-> Pipes.print