【问题标题】:haskell optimise code and stack overflow in tail recursionhaskell 优化尾递归中的代码和堆栈溢出
【发布时间】:2018-06-05 16:41:55
【问题描述】:

当我尝试学习函数式编程时,我决定在 haskell 中应对代码挑战。

在挑战5时https://adventofcode.com/2017/day/5 输入数据https://adventofcode.com/2017/day/5/input 我遇到了几个问题。

这是我的代码

import Data.Array
import System.IO

listToArray l =
  let n_elem = length l
      pos_val = zip (range (0, n_elem)) l
  in array (0, n_elem-1) pos_val

getData filename = do
  s <- readFile filename
  let l = map read (lines s) ::[Int]
      a = listToArray l 
  return a

-- Part 1

updatePosArray i a =
  let i_val = a ! i
  in (i+i_val, a//[(i, i_val + 1)])

solution1 a n_steps i
 | i >= length a || i < 0 = n_steps
 | otherwise =
    let ai = updatePosArray i a
    in solution1 (snd ai) (n_steps+1) (fst ai)

-- Part 2

updatePosArray2 i a =
  let i_val = a ! i
  in
    if i_val>=3 then (i+i_val, a//[(i, i_val-1)])
    else (i+i_val, a//[(i, i_val+1)])

solution2 a n_steps i
 | i >= length a || i < 0 = n_steps
 | otherwise =
    let ai = updatePosArray2 i a
    in solution2 (snd ai) (n_steps+1) (fst ai)


main = do
  x <- getData "/Users/lucapuggini/Documents/AdventOfCode/data/data_ch5_p1.txt"
  let x_ex = array (0,4) [(0, 0), (1, 3), (2, 0), (3, 1), (4, -3)]

  let n_steps_ex1 = solution1 x_ex 0 0
  print $ n_steps_ex1

  let n_steps1 = solution1 x 0 0
  print $ n_steps1

  let n_steps_ex2 = solution2 x_ex 0 0
  print $ n_steps_ex2

  -- very slow. Probably due to the immutable array
  let n_steps2 = solution2 x 0 0
  print $ n_steps2

这是我得到的结果:

lucas-MacBook-Pro:src lucapuggini$ stack runhaskell challenge5.hs

    5
    381680
    10
    stack overflow

代码运行缓慢,但这可能是意料之中的,因为我使用的是不可变数组,但堆栈溢出错误令我感到惊讶。我认为尾递归不应该发生这种情况。

最后我有两个问题:

1) 为什么会出现 stackoverflow 错误?我是否错误地使用了尾递归?

2) 运行此代码的更有效但仍然有效的方法是什么?不可变数组是一个糟糕的选择吗?

我对haskell很陌生,所以请清楚。

【问题讨论】:

  • Haskell 中的“栈”不是函数调用栈;这就是尾递归不相关的原因。
  • 运行该代码的正确方法是什么?
  • 这工作lucas-MacBook-Pro:src lucapuggini$ stack ghc challenge5.hs [1 of 1] Compiling Main ( challenge5.hs, challenge5.o ) Linking challenge5 ... lucas-MacBook-Pro:src lucapuggini$ ./challenge5 5 381680 10 29717847
  • 你使用的是什么 Haskell 实现,什么版本?我不认为 GHC 堆栈溢出错误了。
  • 另外,是的,如果您正在编写这样的代码,那么使用不可变数组实际上没有任何意义。切换到可变数组,或者使用更快的纯操作,例如Data.IntMap.Strict

标签: arrays performance haskell recursion


【解决方案1】:

如果您导入Data.Array.Unboxed 而不是Data.Array,并将您的数组声明为

    ....
        a :: UArray Int Int 
        a = listToArray l
    return a

或者在getData 中,您将获得显着的加速,接近于奇迹。

另外,您必须重新实现lengthArr = rangeSize . bounds,因此它适用于未装箱的数组。

【讨论】:

    【解决方案2】:

    关于你的第一个问题(为什么堆栈溢出):

    使用stack runhaskell(或等效的stack runghc)以特殊的“即时”编译模式运行您的代码,与在 GHCi 提示符下输入的表达式的运行方式大致相同。代码未经优化,经常会表现出糟糕的性能特征。

    对于您的特定程序,这意味着它运行非常缓慢,内存占用不断扩大,最终会产生堆栈溢出。

    如果您改为编译和运行:

    stack ghc -- -O2 challenge5.hs
    ./challenge5
    

    您会发现它运行得更快(在我的笔记本电脑上大约需要一分钟),在恒定内存中,而且显然没有堆栈溢出。

    正如 cmets 中所指出的,GHC 中的堆栈溢出错误实际上与尾递归没有任何关系。相反,它源于惰性求值的特定方面。 (例如,请参阅Do stack overflow errors occur in Haskell?。)

    简而言之,GHC 会创建“thunk”,代表未计算的表达式,其值可以在未来某个时间点被要求。有时,这些 thunk 以这样的方式链接在一起,形成一条长链,当需要链一端的 thunk 的值时,需要对链下的所有 thunk 进行“部分评估”到链的末端在程序可以开始计算 thunk 值之前获取最后一个 thunk 的值。 GHC 将所有这些“正在进行的 thunk 评估”保存在一个大小有限的堆栈中,并且可能会溢出。

    触发堆栈溢出的一个简单例子是:

    -- Sum.hs
    main = print $ sum [1..100000000]
    

    如果你运行这个:

    stack runhaskell -- Sum.hs      # using GHC version 8.0.2
    

    你会得到:

    Sum.hs: stack overflow
    

    但是,使用 ghc -O2 编译它就足以解决问题(对于 Sum.hs 和您的原始程序)。原因之一可能是应用了“严格性分析”优化,该优化会强制提早进行 thunk,因此无法形成这些长链。

    关于你的第二个问题(不可变数组是正确的做法):

    正如@WillNess 指出的那样,使用未装箱数组代替装箱数组可以显着提高性能:在我的笔记本电脑上,您的代码的未装箱版本运行时间为 8 秒,而 63 秒。

    但是,使用这种类型的算法——基本上,大量的小变化以增量方式对向量进行,使得所做的变化取决于累积的先前变化的整个历史——你可以使用 mutable 数组做得更好。我有一个使用 Data.Vector.Unboxed.Mutable 的版本,它在 0.12 秒内运行第 2 部分,您应该能够使用来自 Data.Array.Unboxed 的可变未装箱数组实现类似的性能。

    【讨论】:

    • 可变数组似乎是最合理的选择。但我认为在函数式编程中应该避免可变结构
    • 赞同这个观点的人实际上会说在all编程中应该避免可变结构。在函数式语言中更容易。论点(我不会在这里提出!)是不可变算法导致更简洁的设计和更少的错误,并且复制数据以进行小的更改的潜在低效率通常可以通过仔细的数据结构设计和聪明的编译器进行优化。但是,使用可变数据结构仍然更容易实现某些算法,因此我们选择“谨慎”使用它们。
    • 问题在于实现,而对于未装箱的不可变数组aa // [(i,e)] in tail position 应该是 O(1),但不是。
    猜你喜欢
    • 2015-11-30
    • 2019-07-08
    • 2015-08-05
    • 1970-01-01
    • 2015-04-04
    • 1970-01-01
    • 2015-01-07
    • 2012-07-08
    • 2016-11-09
    相关资源
    最近更新 更多