Haskell:从具有一百万个值的列表构造IntMap时,是否应该出现“堆栈空间溢出”的情况?

时间:2018-08-15 23:18:37

标签: haskell stack-overflow

我的问题是,在Haskell中使用任何Map实现时,使用百万个值时,总会出现“堆栈空间溢出”的情况。

我想做的是处理成对的列表。每对包含两个Int(不是Integer,我对它们的失败很惨,所以我尝试了Ints)。我想遍历列表中的每对,并使用第一个Int作为键。对于每个唯一键,我想建立一个第二元素列表,其中每个第二元素成对且具有相同的第一元素。因此,我最后想要的是从一个Int到一个Int列表的“映射”。这是一个例子。

给出这样的配对列表:

[(1,10),(2,11),(1,79),(3,99),(1,42),(3,18)]

我想得到一个这样的“地图”:

{1 : [42,79,10], 2 : [11], 3 : [18,99]}

(我在上面使用类似Python的符号来说明“地图”。我知道它不是Haskell。它只是出于说明目的。)

所以我尝试的第一件事是我自己手工制作的版本,在该版本中,我对整数对的列表进行了排序,然后遍历该列表以建立一个新的对对列表,但是这次第二个元素是一个列表。第一个元素是键,即每对第一个元素的唯一Int值,第二个元素是将键作为第一个元素的每个原始对的第二个值的列表。

因此给出了这样的配对列表:

[(1,10),(2,11),(1,79),(3,99),(1,42),(3,18)]

我最终得到了这样的配对列表:

[(1, [42,79,10], (2, [11]), (3, [18,99])]

这很容易做到。但是有一个问题。原始列表(1000万对)中的“排序”功能的性能令人震惊地糟糕。我可以在不到一秒钟的时间内生成原始的配对列表。我可以在不到一秒钟的时间内将排序后的列表处理到我的手工地图中。但是,对原始的配对列表进行排序需要40秒。

因此,我想到了使用Haskell中的内置“地图”数据结构之一来完成这项工作。我的想法是先建立原始的配对列表,然后使用标准Map函数构建标准Map。

这就是所有梨形的地方。它可以很好地在100,000个值的列表上工作,但是当我移至100万个值时,会出现“堆栈空间溢出”错误。

所以这是受此问题困扰的一些示例代码。请注意,这不是我要实现的实际代码。它只是存在相同问题的非常简化的代码版本。我真的不想将一百万个连续数字分隔为奇数和偶数分区!

import Data.IntMap.Strict(IntMap, empty, findWithDefault, insert, size)

power = 6

ns :: [Int]
ns = [1..10^power-1]

mod2 n = if odd n then 1 else 0

mod2Pairs = zip (map mod2 ns) ns

-- Takes a list of pairs and returns a Map where the key is the unique Int values
-- of the first element of each pair and the value is a list of the second values
-- of each pair which have the key as the first element.
-- e.g. makeMap [(1,10),(2,11),(1,79),(3,99),(1,42),(3,18)] = 
--      1 -> [42,79,10], 2 -> [11], 3 -> [18,99]
makeMap :: [(Int,a)] -> IntMap [a]
makeMap pairs = makeMap' empty pairs
  where
    makeMap' m [] = m
    makeMap' m ((a, b):cs) = makeMap' m' cs
      where
        bs = findWithDefault [] a m
        m' = insert a (b:bs) m

mod2Map = makeMap mod2Pairs

main = do
  print $ "Yowzah"
  print $ "length mod2Pairs="++ (show $ length mod2Pairs)
  print $ "size mod2Map=" ++ (show $ size mod2Map)

运行此命令时,我得到:

"Yowzah"
"length mod2Pairs=999999"
Stack space overflow: current size 8388608 bytes.
Use `+RTS -Ksize -RTS' to increase it.

从上面的输出中,应该很清楚,当我尝试执行“ makeMap mod2Pairs”时,会发生堆栈空间溢出。

但是,我天真地认为,这似乎是遍历一个对的列表,并为每个对查找一个键(每个对的第一个元素),如果找不到匹配项,则返回A),返回一个空列表或B)如果确实找到匹配项,则返回以前插入的列表。无论哪种情况,它都将“对”的第二个元素“约束”到“找到”列表中,并使用相同的键将其插入Map中。

(PS代替findWithDefault,我也尝试过查找并使用用例处理了Just and Nothing,但无济于事。)

我浏览了有关各种Map实现的Haskell文档,并从CPU和内存(尤其是堆栈内存)的性能角度看,似乎A)是严格的实现,B)一个关键是整数的地方最好。我也尝试过Data.Map和Data.Strict.Map,它们也遇到相同的问题。

我确信问题出在“地图”实现上。我对吗?为什么会出现堆栈溢出错误,即Map实现在后台导致堆栈溢出的原因是什么?它在幕后进行了很多递归调用吗?

任何人都可以帮助解释发生了什么以及如何解决该问题吗?

1 个答案:

答案 0 :(得分:6)

我没有足够老的GHC来检查(这对我来说很好,并且我没有7.6.3像你一样),但是我猜想你的makeMap'也是懒。可能会解决此问题:

makeMap' m ((a, b):cs) = m `seq` makeMap' m' cs

没有它,您将建立一百万个深度的嵌套thunk,而深度嵌套的thunk是导致Haskell堆栈溢出的传统方法。

或者,我尝试将整个makeMap实现替换为fromListWith

makeMap pairs = fromListWith (++) [(k, [v]) | (k, v) <- pairs]