让我们说我有两个功能:
f :: [a] -> b
g :: [a] -> c
我想编写一个与此相当的函数:
h x = (f x, g x)
但是当我这样做时,对于大型名单,我不可避免地会耗尽内存。
一个简单的例子如下:
x = [1..100000000::Int]
main = print $ (sum x, product x)
我理解这种情况是因为列表x
存储在内存中而没有被垃圾回收。最好不要f
而g
在x
上工作,以及“并行”。
假设我无法更改f
和g
,也不想单独制作x
副本(假设x
制作费用昂贵)我该怎么写? h
没有遇到内存不足问题?
答案 0 :(得分:12)
简短的回答是你不能。由于您无法控制f
和g
,因此无法保证函数按顺序处理其输入。在产生最终结果之前,这样的函数也可以将整个列表保存在内存中。
但是,如果您的功能表示为折叠,则情况会有所不同。这意味着我们知道如何逐步应用每个步骤,因此我们可以在一次运行中并行化这些步骤。
关于这个领域有很多资源。例如:
使用管道类库(例如 conduit , iteratees 或 pipe >)来消耗具有正确定义的空间边界的一系列值的模式更为普遍。 EM>。例如,在 conduit 中,您可以将计算总和与产品的组合表示为
import Control.Monad.Identity
import Data.Conduit
import Data.Conduit.List (fold, sourceList)
import Data.Conduit.Internal (zipSinks)
product', sum' :: (Monad m, Num a) => Sink a m a
sum' = fold (+) 0
product' = fold (*) 1
main = print . runIdentity $ sourceList (replicate (10^6) 1) $$
zipSinks sum' product'
答案 1 :(得分:2)
您可以使用多个线程并行评估f x
和g x
。
E.g。
x :: [Int]
x = [1..10^8]
main = print $ let a = sum x
b = product x
in a `par` b `pseq` (a,b)
这是利用GHC的并行运行时通过一次做两件事来防止空间泄漏的好方法。
或者,您需要将f
和g
融合到a single pass中。
答案 2 :(得分:2)
如果您可以将功能转换为折叠,则只需将其用于扫描:
x = [1..100000000::Int]
main = mapM_ print . tail . scanl foo (a0,b0) . takeWhile (not.null)
. unfoldr (Just . splitAt 1000) -- adjust the chunk length as needed
$ x
foo (a,b) x = let a2 = f' a $ f x ; b2 = g' b $ g x
in a2 `seq` b2 `seq` (a2, b2)
f :: [t] -> a -- e.g. sum
g :: [t] -> b -- (`rem` 10007) . product
f' :: a -> a -> a -- e.g. (+)
g' :: b -> b -> b -- ((`rem` 10007) .) . (*)
我们以块的形式使用输入以获得更好的性能。使用-O2
编译,这应该在一个恒定的空间中运行。印刷中期结果作为进展的指示。
如果你无法将你的功能变成折叠,这意味着它有使用整个列表来产生任何输出,这个技巧不适用。