我是函数式编程的新手,现在学习Haskell。作为练习,我决定实现一维线性扩散方程的显式欧拉方法。虽然下面的代码工作正常,但我对它的性能并不满意。事实上,我关心的是内存消耗。我相信它与懒惰的评估有关,但无法弄清楚如何减少其内存使用量。
算法的想法非常简单,用命令式术语表达:它采用“数组”,并且每个内部点都添加一个值,该值是作为点本身中的值的组合计算的在它的邻居。边界点是特殊情况。
所以,这是我的Euler1D.hs模块:
module Euler1D
( stepEuler
, makeu0
) where
-- impose zero flux condition
zeroflux :: (Floating a) => a -> [a] -> [a]
zeroflux mu (boundary:inner:xs) = [boundary+mu*2*(inner-boundary)]
-- one step of integration
stepEuler :: (Floating a) => a -> [a] -> [a]
stepEuler mu u@(x:xs) = (applyBC . (diffused mu)) u
where
diffused mu (left:x:[]) = [] -- ignore outer points
diffused mu (left:x:right:xs) = -- integrate inner points
(x+mu*(left+right-2*x)) : diffused mu (x:right:xs)
applyBC inner = (lbc u') ++ inner ++ (rbc u') -- boundary conditions
where u' = [head u] ++ inner ++ [last u]
lbc = zeroflux mu -- left boundary
rbc = (zeroflux mu) . reverse -- right boundary
-- initial condition
makeu0 :: Int -> [Double]
makeu0 n = [ ((^2) . sin . (pi*) . xi) x | x <- [0..n]]
where xi x = fromIntegral x / fromIntegral n
简单的Main.hs:
module Main where
import System ( getArgs )
import Euler1D
main = do
args <- getArgs
let n = read $ head args :: Int
let u0 = makeu0 n
let un = stepEuler 0.5 u0
putStrLn $ show $ sum un
为了比较,我还写了a pure C implementation。
现在,如果我尝试为足够大的数组n
运行Haskell实现,我有:
$ time ./eulerhs 200000
100000.00000000112
real 0m3.552s
user 0m3.304s
sys 0m0.128s
为了进行比较,C版本的速度提高了近两个数量级:
$ time ./eulerc 200000
100000
real 0m0.088s
user 0m0.048s
sys 0m0.008s
编辑:这种比较并不公平,因为Haskell版本是 用剖析标志和C编译 不是。如果我编译两个程序 使用
-O2
并且两者都没有分析 旗帜,我可以增加n
。在这 案例time ./eulerhs 1000000
需要 0m2.236s,而time ./eulerc 1000000
仅需0m0.293s。所以 问题仍然存在 优化和不分析, 它只是抵消了。我还要注意,那个记忆 分配Haskell程序 似乎与
n
一致。 这可能没问题。
但最糟糕的是内存要求。我的Haskell版本需要超过100MB (我估计C中的最小值 4MB )。我认为这可能是问题的根源。根据分析报告,该程序在GC中花费了85%的时间,并且
total time = 0.36 secs (18 ticks @ 20 ms)
total alloc = 116,835,180 bytes (excludes profiling overheads)
COST CENTRE MODULE %time %alloc
makeu0 Euler1D 61.1 34.9
stepEuler Euler1D 33.3 59.6
CAF:sum Main 5.6 5.5
我很惊讶地发现makeu0
非常昂贵。我认为这是由于它的懒惰评估(如果它的thunk在stepEuler
结束之前仍保留在内存中。)
我在Main.hs
:
let un = u0 `seq` stepEuler 0.5 u0
但没有发现任何差异。我不知道如何减少stepEuler
中的内存使用量。所以,我的问题是:
seq
和刘海,在哪里以及为什么?我确实在Real World Haskell中阅读了关于性能分析和优化的章节,但目前还不清楚我究竟能够确定什么应该是严格的,什么不是。
请原谅我这么长的帖子。
EDIT2 :正如A. Rex在评论中所建议的那样,我尝试了同时运行它们 valgrind的程序。这是什么 我观察到了。对于Haskell程序 (
n
= 200000)它找到了:malloc / free:33个allocs,30个frees,84,109个字节。 ... 检查了55,712,980字节。
对于C程序(经过小修复):
malloc / free:分配2个allocs,2个frees,3,200,000个字节。
所以,看起来虽然是Haskell 分配更小的内存块, 它经常这样做,并且由于延迟 垃圾收集,他们积累 并留在记忆中。所以我有 另一个问题:
- 是否可以避免很多 Haskell中的小额分配? 基本上,要声明,我需要 处理整个数据结构 而不仅仅是它的片段 需求。
答案 0 :(得分:19)
列表不是此类代码的最佳数据结构(有很多(++)和(最后))。你失去了大量的时间来构建和解构列表。我使用Data.Sequence或数组,如C版本。
没有机会对makeu0的thunk进行垃圾收集,因为你需要保留所有这些(好吧,所有结果都是“漫反射”,确切地说)直到最后计算,以便能够在applyBC中做“反向”。这是非常昂贵的事情,考虑到您只需要列表尾部的两个项目为您的“zeroflux”。
这是快速破解您的代码,试图实现更好的列表融合并减少列表(de)构建:
module Euler1D
( stepEuler
) where
-- impose zero flux condition
zeroflux mu (boundary:inner:xs) = boundary+mu*2*(inner-boundary)
-- one step of integration
stepEuler mu n = (applyBC . (diffused mu)) $ makeu0 n
where
diffused mu (left:x:[]) = [] -- ignore outer points
diffused mu (left:x:right:xs) = -- integrate inner points
let y = (x+mu*(left+right-2*x))
in y `seq` y : diffused mu (x:right:xs)
applyBC inner = lbc + sum inner + rbc -- boundary conditions
where
lbc = zeroflux mu ((f 0 n):inner) -- left boundary
rbc = zeroflux mu ((f n n):(take 2 $ reverse inner)) -- right boundary
-- initial condition
makeu0 n = [ f x n | x <- [0..n]]
f x n = ((^2) . sin . (pi*) . xi) x
where xi x = fromIntegral x / fromIntegral n
对于200000点,它在0.8秒内完成,初始版本为3.8秒
答案 1 :(得分:9)
在我的32位x86系统上,您的程序仅使用大约40 MB的内存。
您是否可能会混淆分析输出中的“total alloc = 116,835,180 bytes”行,以及程序在任何时候实际使用了多少内存?总alloc是在整个程序运行中分配的内存量;随着你的进展,垃圾收集器释放了大部分内容。你可以期望这个数字在Haskell程序中变得非常大;我有程序在整个运行过程中分配了许多TB的内存,尽管它们实际上最大虚拟内存大小为100兆左右。
在程序运行过程中,我不会过分担心大量的总分配;这是纯语言的本质,GHC的运行时有一个非常好的垃圾收集器来帮助弥补这一点。
答案 2 :(得分:4)
更一般地说,你可以找到你的记忆使用位置GHC's heap profiling tools.根据我的经验,他们不一定会告诉你为什么你的数据被泄露,但至少可以缩小潜在的原因。 / p>
您可能还会发现这个excellent blog post by Don Stewart有助于理解严格性,它与垃圾收集的交互方式,以及如何诊断和解决问题。
答案 3 :(得分:2)
使用$强制“非懒惰”!救命?根据{{3}}。
答案 4 :(得分:1)
Per Harleqin的要求:您是否尝试过设置优化标志?例如,使用ghc,您可以使用添加“-O2”,就像使用gcc一样。 (虽然我不确定ghc中存在哪些优化级别;手册页并没有确切地说......)
根据我过去的经验,设置此标志会产生巨大的差异。据我所知,runhugs
和未经优化的ghc
使用Haskell最基本,最明显的实现;不幸的是,这有时效率不高。
但这只是猜测。正如我在评论中所说,我希望有人能够很好地回答你的问题。我经常在分析和修复Haskell的内存使用方面遇到问题。
答案 5 :(得分:1)
也请使用开关-fvia-C
。
答案 6 :(得分:0)
现在跳到我眼前的一件事是Haskell输出是一个浮点数,而C输出似乎是整数。我还没有掌握Haskell代码,但有可能在Haskell中有一些浮点运算,而C使用整数吗?