记忆完成了,现在怎么办?

时间:2014-03-18 11:13:30

标签: performance haskell memoization

我试图在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);
}
}

1 个答案:

答案 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时间(或生产力)

最大驻留时间是衡量程序需要多少内存的指标。 %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代码看看它正在执行哪些计算会很有趣。