繁体   English   中英

Haskell:IORefs的表现

[英]Haskell: performance of IORefs

我一直在尝试在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.2s并使用290MB内存,而main2只需0.4s并且仅使用1MB。 是否有任何阻止这种增长的技巧,特别是在太空? 我经常需要IORef用于非原始类型,而不像Int ,并假设IORef会像常规thunk一样使用额外的指针,但我的直觉似乎是错误的。

我已经尝试了一个带有解压缩的IORef的专用列表类型,但没有显着差异。

这很可能不是关于IORef ,而是关于严格性。 IO monad中的操作是串行的 - 所有先前的操作必须在下一个操作开始之前完成。 所以

mapM newIORef list

在读取任何内容之前产生一百万个IORef

然而,

map runIdentity . map Identity
= map (runIdentity . Identity)
= map id

流非常好,所以我们print列表中的一个元素,然后生成下一个元素,等等。

如果您想要更公平的比较,请使用严格的map

map' :: (a -> b) -> [a] -> [b]
map' f [] = []
map' f (x:xs) = (f x:) $! map' f xs

问题是你使用mapM ,它在时间和空间上总是在大型列表上表现不佳。 正确的解决方案是使用mapM_(>=>)融合中间列表:

import Data.IORef
import Control.Monad

list :: [Int]
list = [1..10^6]

main = mapM_ (newIORef >=> readIORef >=> print) list

它在恒定的空间内运行并提供出色的性能,在我的机器上运行0.4秒。

编辑:在回答您的问题时,您也可以使用pipes来避免必须手动融合循环:

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秒的恒定空间运行。

我发现解决问题的方法是使用惰性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)

这允许monadic版本在相同的1MB和相似的时间内运行。 我希望懒惰的ST monad可以在不使用unsafeInterleaveIO情况下更优雅地解决这个问题,作为一个函数:

main = print $ runST (mapM (newSTRef) list >>= mapM (readSTRef))

但这不起作用(你还需要使用unsafeInterleaveST ),这让我想到了Control.Monad.ST.Lazy究竟是多么懒惰。 有人知道吗? :)

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM