【发布时间】:2014-09-27 14:42:41
【问题描述】:
我有一个简单的玩具示例,它似乎与垃圾收集器不同意可以回收哪些数据结构(也称为内存泄漏)。我并不是想提出这个算法的内存效率更高的版本(这里有一组更好的算法:Haskell Wiki - Prime numbers,而是解释为什么垃圾收集器没有识别旧的、超出范围和未使用的部分列表以回收该内存。
代码在这里:
import Data.List (foldl')
erat' :: (Integer, Bool) -> [(Integer,Integer)] -> [(Integer,Integer)]
erat' (c,b) ((x,y):xs)
| c < x = (x,y) : erat' (c,b) xs
| c == x = (x+y,y) : erat' (c,True) xs
| c > x = (x+y,y) : erat' (c,b) xs
erat' (c,b) []
| b = []
| otherwise = [(c,c)]
erat :: [Integer] -> [(Integer,Integer)]
erat = foldl' (\a c -> erat' (c,False) a) []
primes :: Integer -> [Integer]
primes n = map snd $ erat [2..n]
本质上,使用正整数调用素数将返回一个包含该数之前的所有素数的列表。素数对及其高水位标记倍数的列表被传递给 erat',以及一对包括候选和布尔值(素数为假,非素数为真)。每个对 erat' 的非递归调用都会传递一个新列表,我希望输出最多包含从列表开头到第一次更改点的某些共享单元。
一旦列表中的已修改单元格传递给 erat' 超出范围,内存应该被标记为恢复,但是当您尝试调用具有足够大数量(例如 1,000,000)的素数时可以看到),内存利用率会迅速飙升至数十 GB。
现在,问题是:为什么会发生这种情况?分代垃圾收集器不应该检测取消引用的列表单元来回收它们吗?而且,它是否应该很容易检测到它们没有引用,因为:
a) 没有任何东西可以从比它自己更早的数据结构中引用; b) 不能有更新的引用,因为这些单元格/片段甚至不再是可引用数据结构的一部分,因为它超出了范围?
当然,可变数据结构可以解决这个问题,但我觉得在这种情况下诉诸可变性会使 Haskell 的一些理论原则落空。
感谢评论的人(尤其是 Carl),我稍微修改了算法以增加严格性(以及开始穿过新素数的平方的优化,因为低倍数也会被低素数的倍数穿过)。
这是新版本:
import Data.List (foldl')
erat' :: (Integer, Bool) -> [(Integer,Integer)] -> [(Integer,Integer)]
erat' (c,b) ((x,y):xs)
| c < x = x `seq` (x,y) : erat' (c,b) xs
| c == x = x `seq` (x+y,y) : erat' (c,True) xs
| c > x = x `seq` (x+y,y) : erat' (c,b) xs
erat' (c,b) []
| b = []
| otherwise = [(c*c,c)] -- lower multiples would be covered by multiples of lower primes
erat :: [Integer] -> [(Integer,Integer)]
erat = foldl' (\a c -> erat' (c,False) a) []
primes :: Integer -> [Integer]
primes n = map snd $ erat [2..n]
内存消耗似乎仍然相当大。此算法是否有任何其他更改可以帮助降低总内存利用率?
由于 Will 指出我没有提供完整的统计数据,因此这些是上面列出的更新版本的素数运行的数字,参数为 100000:
在应用 Will 提出的更改后,内存使用量现在大幅下降。例如,再次查看 100000 的素数运行:
最后,这是合并提议的更改后的最终代码:
import Data.List (foldl')
erat'' :: (Integer, Bool) -> [(Integer,Integer)] -> [(Integer,Integer)]
erat'' (c,b) ((x,y):xs)
| c < x = (x, y) : if x==y*y then (if b then xs
else xs++[(c*c,c)])
else erat'' (c,b) xs
| c == x = (x+y,y) : if x==y*y then xs
else erat'' (c,True) xs
| c > x = (x+y,y) : erat'' (c,b) xs
erat'' (c,True) [] = []
erat'' (c,False) [] = [(c*c,c)]
primes'' :: Integer -> [Integer]
primes'' n = map snd $ foldl' (\a c -> (if null a then 0 else
case last a of (x,y) -> y) `seq` erat'' (c,False) a) [] [2..n]
最后是跑 1,000,000 来感受一下这个新版本的性能:
【问题讨论】:
-
如果 Haskell 是一种严格的语言,你的分析是正确的,但事实并非如此。
-
我尝试使用 seq 和 deepseq 更改为严格,结果相似。我不认为非严格在这里起作用,但我可能错了。您愿意详细说明吗?
-
乍一看,这似乎对
foldl'的使用非常糟糕。你试过foldr吗? -
我当然可以试试 foldr。理想情况下,为了清楚起见,我更愿意从左侧解析列表,这是我最初使用显式递归所做的,然后为了简洁起见将其转换为 foldl。
-
Ghci,最初。但是后来用 ghc 编译了可执行文件并从命令行运行它,因为我想把 ghci 从图片中删除。我使用 -O2 作为编译器优化标志。
标签: algorithm haskell garbage-collection primes sieve-of-eratosthenes