Performance Haskell:IORefs的性能

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

我一直在尝试用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 >>= 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