我试图在Haskell中解决一个难题,并编写了以下代码:
u 0 p = 0.0
u 1 p = 1.0
u n p = 1.0 + minimum [((1.0-q)*(s k p)) + (u (n-k) p) | k <-[1..n], let q = (1.0-p)**(fromIntegral k)]
s 1 p = 0.0
s n p = 1.0 + minimum [((1.0-q)*(s (n-k) p)) + q*((s k p) + (u (n-k) p)) | k <-[1..(n-1)], let q = (1.0-(1.0-p)**(fromIntegral k))/(1.0-(1.0-p)**(fromIntegral n))]
但这段代码非常慢。我怀疑这是因为一次又一次地计算相同的事情。因此,我做了一个记忆版本:
memoUa = array (0,10000) ((0,0.0):(1,1.0):[(k,mua k) | k<- [2..10000]])
mua n = (1.0) + minimum [((1.0-q)*(memoSa ! k)) + (memoUa ! (n-k)) | k <-[1..n], let q = (1.0-0.02)**(fromIntegral k)]
memoSa = array (0,10000) ((0,0.0):(1,0.0):[(k,msa k) | k<- [2..10000]])
msa n = (1.0) + minimum [((1.0-q) * (memoSa ! (n-k))) + q*((memoSa ! k) + (memoUa ! (n-k))) | k <-[1..(n-1)], let q = (1.0-(1.0-0.02)**(fromIntegral k))/(1.0-(1.0-0.02)**(fromIntegral n))]
这似乎要快得多,但现在我出现了内存不足的错误。我不明白为什么会发生这种情况(java中的相同策略,没有递归,没有问题)。有人能指出我如何改进这段代码的正确方向吗?
编辑:我在这里添加我的java版本(因为我不知道在哪里放置它)。我意识到这些代码并不是读者友好的(没有有意义的名字等),但我希望它足够清楚。public class Main {
public static double calc(double p) {
double[] u = new double[10001];
double[] s = new double[10001];
u[0] = 0.0;
u[1] = 1.0;
s[0] = 0.0;
s[1] = 0.0;
for (int n=2;n<10001;n++) {
double q = 1.0;
double denom = 1.0;
for (int k = 1; k <= n; k++ ) {
denom = denom * (1.0 - p);
}
denom = 1.0 - denom;
s[n] = (double) n;
u[n] = (double) n;
for (int k = 1; k <= n; k++ ) {
q = (1.0 - p) * q;
if (k<n) {
double qs = (1.0-q)/denom;
double bs = (1.0-qs)*s[n-k] + qs*(s[k]+ u[n-k]) + 1.0;
if (bs < s[n]) {
s[n] = bs;
}
}
double bu = (1.0-q)*s[k] + 1.0 + u[n-k];
if (bu < u[n]) {
u[n] = bu;
}
}
}
return u[10000];
}
public static void main(String[] args) {
double s = 0.0;
int i = 2;
//for (int i = 1; i<51; i++) {
s = s + calc(i*0.01);
//}
System.out.println("result = " + s);
}
}
答案 0 :(得分:4)
当我运行编译版本时,我的内存不足,但Java版本的工作方式与Haskell版本的工作方式之间存在显着差异,我将在此处说明。
首先要做的是添加一些重要的类型签名。特别是,您不希望Integer
数组索引,所以我添加了:
memoUa :: Array Int Double
memoSa :: Array Int Double
我使用ghc-mod check
找到了这些内容。我还添加了一个main
,以便您可以从命令行运行它:
import System.Environment
main = do
(arg:_) <- getArgs
let n = read arg
print $ mua n
现在,为了深入了解正在发生的事情,我们可以使用性能分析编译程序:
ghc -O2 -prof memo.hs
然后当我们调用这样的程序时:
memo 1000 +RTS -s
我们将获得如下所示的分析输出:
164.31333233347755
98,286,872 bytes allocated in the heap
29,455,360 bytes copied during GC
657,080 bytes maximum residency (29 sample(s))
38,260 bytes maximum slop
3 MB total memory in use (0 MB lost due to fragmentation)
Tot time (elapsed) Avg pause Max pause
Gen 0 161 colls, 0 par 0.03s 0.03s 0.0002s 0.0011s
Gen 1 29 colls, 0 par 0.03s 0.03s 0.0011s 0.0017s
INIT time 0.00s ( 0.00s elapsed)
MUT time 0.21s ( 0.21s elapsed)
GC time 0.06s ( 0.06s elapsed)
RP time 0.00s ( 0.00s elapsed)
PROF time 0.00s ( 0.00s elapsed)
EXIT time 0.00s ( 0.00s elapsed)
Total time 0.27s ( 0.27s elapsed)
%GC time 21.8% (22.3% elapsed)
Alloc rate 468,514,624 bytes per MUT second
Productivity 78.2% of total user, 77.3% of total elapsed
需要注意的重要事项是:
最大驻留时间是衡量程序需要多少内存的指标。 %GC时间占垃圾收集时间的比例和生产率是补充(100% - %GC时间)。
如果针对各种输入值运行程序,您将看到大约80%的生产率:
n Max Res. Prod. Time Output
2000 779,076 79.4% 1.10s 328.54535361588535
4000 1,023,016 80.7% 4.41s 657.0894961398351
6000 1,299,880 81.3% 9.91s 985.6071032981068
8000 1,539,352 81.5% 17.64s 1314.0968411684714
10000 1,815,600 81.7% 27.57s 1642.5891214360522
这意味着大约20%的运行时间花在垃圾收集上。此外,随着n
的增加,我们看到内存使用量不断增加。
事实证明,通过告诉Haskell评估数组元素的顺序而不依赖于延迟评估,我们可以显着提高生产力和内存使用率:
import Control.Monad (forM_)
main = do
(arg:_) <- getArgs
let n = read arg
forM_ [1..n] $ \i -> mua i `seq` return ()
print $ mua n
新的分析统计数据是:
n Max Res. Prod. Time Output
2000 482,800 99.3% 1.31s 328.54535361588535
4000 482,800 99.6% 5.88s 657.0894961398351
6000 482,800 99.5% 12.09s 985.6071032981068
8000 482,800 98.1% 21.71s 1314.0968411684714
10000 482,800 96.1% 34.58s 1642.5891214360522
这里有一些有趣的观察:生产力提高,内存使用率下降(现在在输入范围内保持不变),但运行时间增加。这表明我们强制进行了比我们需要的计算更多的计算。在像Java这样的命令式语言中,您必须提供评估顺序,以便准确了解需要执行哪些计算。看到你的Java代码看看它正在执行哪些计算会很有趣。