如何在常量内存中获取make stats

时间:2010-10-24 13:40:29

标签: haskell random statistics memory-management lazy-evaluation

我有一个函数,可以创建一些随机数值结果。我知道,结果将是(小,a - b约50)范围 a,b 的整数。我想创建一个执行上述函数的函数,比方说1000000次并计算每个结果出现的频率。 (该函数采用随机生成器来生成结果。)问题是,我不知道如何在常量内存中执行此操作而不对范围的长度进行硬编码。我的(坏)方法是这样的:

values :: [Int]
values = doFunctionNtimes myRandom 1000000
results = map (\x ->length . filter (x==) $ values) [a..b]

有人有这个想法吗?

编辑:

我想我错误地解释了这个问题,对不起。我有一个函数,它取决于一个随机的gen - 给出一些小的int值。为了统计,我想知道结果出现的频率。因为我想让统计数据超过1000000次尝试,我需要在尝试次数上保持不变的记忆。

4 个答案:

答案 0 :(得分:9)

import qualified Data.Map as Map
import Data.List (foldl')          -- ' (to fix SO syntax highlighting)

histogram :: (Ord a) => [a] -> Map.Map a Int
histogram = foldl' (\m x -> Map.insertWith' (+) x 1 m) Map.empty

为什么这种方法有效以及为什么它优于特拉维斯·布朗的解决方案的解释非常具有技术性,需要耐心才能完全理解。

如果列表中可能出现的值有限,那么这将在常量内存中运行。特拉维斯的解决方案有一个微妙的错误,其中生成的地图条目将如下所示:

(4, 1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1)

数字19的效率非常低。只有当您在地图中要求该元素时才会计算出巨大的总和。这些“thunk”(延迟评估表达式)将随着输入的大小线性增长。

为了防止这种情况,我们使用insertWith',它严格地应用函数,也就是说它在将结果放入地图之前对结果进行求值。那么如果你在上面的地图中插入4,它将评估thunk,你会得到一个很好的整洁:

(4, 20)

另一个人会在添加之前对其进行评估,以便获得:

(4, 21)

所以现在至少地图的值是恒定的空间。

我们需要做的最后一件事是将右侧折叠更改为左侧折叠,因为Map.insert在其第二个参数中是严格的。以下说明右折叠的含义。

iw x m = Map.insertWith' (+) x 1 m    -- '

foldr iw Map.empty [1,2,1,3,2,1]
    = iw 1 (iw 2 (iw 1 (iw 3 (iw 2 (iw 1 Map.empty)))))

使用iw作为简单的简写。 Map.insert在其第二个参数中是严格的意味着您需要在插入可以执行任何工作之前评估要插入的映射。我将使用符号{ k1 -> v1, k2 -> v2, ... }作为地图的简写。您的评估顺序如下所示:

foldr f z [] = z
foldr f z (x:xs) = f x (foldr f z xs)

foldr iw {} [1,2,1,3,2,1]
iw 1 (foldr iw {} [2,1,3,2,1])
iw 1 (iw 2 (foldr iw {} [1,3,2,1]))
iw 1 (iw 2 (iw 1 (foldr iw {} [3,2,1])))
iw 1 (iw 2 (iw 1 (iw 3 (foldr iw {} [2,1]))))
iw 1 (iw 2 (iw 1 (iw 3 (iw 2 (foldr iw {} [1])))))
iw 1 (iw 2 (iw 1 (iw 3 (iw 2 (iw 1 (foldr iw {} []))))))
iw 1 (iw 2 (iw 1 (iw 3 (iw 2 (iw 1 {}))))))
iw 1 (iw 2 (iw 1 (iw 3 (iw 2 {1 -> 1}))))
iw 1 (iw 2 (iw 1 (iw 3 {1 -> 1, 2 -> 1})))
iw 1 (iw 2 (iw 1 {1 -> 1, 2 -> 1, 3 -> 1}))
iw 1 (iw 2 {1 -> 2, 2 -> 1, 3 -> 1})
iw 1 {1 -> 2, 2 -> 2, 3 -> 1}
{1 -> 3, 2 -> 2, 3 -> 1}

因此,如果你有一个1,000,000个元素数组,我们必须一直到第1,000,000个元素才能开始插入,因此我们需要将之前的999,999个元素保留在内存中,以便我们知道下一步该做什么。左折叠解决了这个问题:

-- definition of left fold
foldl' f z xs = go z xs             -- '
    where 
    go accum [] = z
    go accum (x:xs) = accum `seq` go (f accum x) xs

foldl' (flip iw) Map.empty [1,2,1,3,2,1]  -- needed to flip arg order to appease foldl'
go {} [1,2,1,3,2,1]
go (iw 1 {}) [2,1,3,2,1]
go (iw 2 {1 -> 1}) [1,3,2,1]
go (iw 1 {1 -> 1, 2 -> 1}) [3,2,1]
go (iw 3 {1 -> 2, 2 -> 1}) [2,1]
go (iw 2 {1 -> 2, 2 -> 1, 3 -> 1}) [1]
go (iw 1 {1 -> 2, 2 -> 2, 3 -> 1}) []
iw 1 {1 -> 2, 2 -> 2, 3 -> 1}
{1 -> 3, 2 -> 2, 3 -> 1}

现在我们可以看到,最后,如果地图中的条目数是有界的,那么它将以恒定的空间和线性时间运行。

答案 1 :(得分:3)

因此,您可以获得无限数量的可能结果,并希望计算每次出现在常量内存中的次数。这显然不可能完全做到,但是名为count-min sketch的数据结构可以用来做很好的近似。在您的情况下,将结果存储在count-min草图中,同时分别跟踪最小值和最大值,最后查询从最小值到最大值的每个整数的count-min草图。

答案 2 :(得分:3)

我通常处理这类问题的方法是跟踪地图中的计数。 Data.IntMap适用于这种情况:

import qualified Data.IntMap as I

results :: [Int] -> I.IntMap Int
results = foldr (\x -> I.insertWith (+) x 1) I.empty

此时,您可以询问范围的端点(I.findMinI.findMax),或者在O(log n)中的特定值处查找计数。将所有内容都放在数组中以便更快地查找也很容易。


更新:有关此代码的更好版本,请参阅luqui's answer

答案 3 :(得分:0)

正如Jouni已经提到的那样,恒定记忆是不可能的,但这个计数最小的草图听起来像炸弹! (虽然我之前没有听说过)。但我认为您可能要求的是将其存储在一个阵列中并且仅更新每个频率的可能性。这可以在具有Mutable数组的haskell中完成。这是一个例子:

main = do gen <- newStdGen
          n <- liftM (read . head) getArgs
          arr  <- (newArray (a,b) 0) :: IO (IOUArray Int Int)
          replicateM_ n $ do 
               result <- myRand
               x <- readArray arr result
               writeArray arr result (x+1)
          (getAssocs arr :: IO [(Int,Int)]) >>= print

使用+ RTS -s运行程序,输入1000000,我们得到输出

787,874,256 bytes allocated in the heap
         364,536 bytes copied during GC
           5,984 bytes maximum residency (1 sample(s))
          17,928 bytes maximum slop
               1 MB total memory in use (0 MB lost due to fragmentation)

...
  Total time    0.29s  (  0.30s elapsed)
...
  %GC time       0.3%  (2.1% elapsed)