【问题标题】:Memoization done, what now?记忆完成了,现在怎么办?
【发布时间】:2014-03-18 11:13:30
【问题描述】:

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

【问题讨论】:

  • 样本输入和预期输出会有所帮助。
  • 类型签名
  • 您是否尝试过使用-O 进行编译,或者您是否在 GHCi 中运行它?
  • 您能否将其分解为几个较小的表达式,例如在where 子句中,或者至少将其拆分为多行?这 190 个字符行很难阅读。
  • Reformatted code,这样可读性更高,但仍然需要类型签名

标签: performance haskell memoization


【解决方案1】:

我在运行编译版本时并没有耗尽内存,但是 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 time 垃圾回收所用时间的比例与Productivity是互补的(100% - %GC time)。

如果您针对不同的输入值运行程序,您将看到大约 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 代码以了解它正在执行哪些计算会很有趣。

【讨论】:

  • 我已经添加了java版本。顺便说一句,我看不到可以完成比需要更多的计算。我希望由于“最低限度”,一切都需要。
  • 我没有看到您将s[k] 设置为一般k 的位置(即不是1 或n)。 (对于数组u[] 也是如此)s[] 的所有其他值是否都等于 0?
猜你喜欢
  • 2016-12-19
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2022-11-09
  • 1970-01-01
相关资源
最近更新 更多