简体   繁体   English

haskell在尾部递归中优化代码和堆栈溢出

[英]haskell optimise code and stack overflow in tail recursion

As I am trying to learn functional programming I have decided to do the advent of code challenges in haskell. 在尝试学习函数式编程时,我决定在haskell中进行代码挑战。

While doing challenge 5 https://adventofcode.com/2017/day/5 with input data https://adventofcode.com/2017/day/5/input I have encountered several problem. 在使用输入数据https://adventofcode.com/2017/day/5/input进行挑战5 https://adventofcode.com/2017/day/5时 ,我遇到了几个问题。

This is my code 这是我的代码

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

and this is the result I get : 这是我得到的结果:

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

    5
    381680
    10
    stack overflow

the code is quiet slow but this is probably expected due to the fact that I am using immutable array but I am surprised by the stack overflow error. 代码是安静的慢速程序,但是这可能是由于我使用不可变数组而引起的,但是我对堆栈溢出错误感到惊讶。 I thought that this should not happen with tail recursion. 我认为尾递归不应该发生这种情况。

In conclusion I have 2 questions: 总之,我有两个问题:

1) Why am I getting stackoverflow error? 1)为什么会出现stackoverflow错误? Am I using tail recursion wrongly? 我是否错误地使用了尾递归?

2) What is a more efficient but still functional way to run this code? 2)什么是更有效但仍可运行的代码运行方式? Are immutable array a bad choice? 不变数组是一个不好的选择吗?

I am very new to haskell so please be clear. 我是Haskell的新手,请注意。

With respect to your first question (why the stack overflow): 关于第一个问题(为什么堆栈溢出):

Using stack runhaskell (or equivalently stack runghc ) runs your code in a special "just in time" compilation mode, much the same way expressions entered at the GHCi prompt are run. 使用stack runhaskell (或等效的stack runghc )以特殊的“及时”编译模式运行代码,与在GHCi提示符下输入的表达式的运行方式几乎相同。 The code is unoptimized and will frequently exhibit terrible performance characteristics. 该代码未经过优化,通常会表现出糟糕的性能特征。

For your particular program, this means it runs very slowly, with an ever-expanding memory footprint, and eventually produces a stack overflow. 对于您的特定程序,这意味着它运行非常缓慢,并且内存占用量不断扩大,最终会导致堆栈溢出。

If you instead compile and run with: 如果改为编译并运行:

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

you'll find that it runs much faster (about a minute on my laptop), in constant memory, and, obviously, without a stack overflow. 您会发现它在恒定的内存中运行得更快(在我的笔记本电脑上大约一分钟),并且显然没有堆栈溢出。

As indicated in the comments, a stack overflow error in GHC doesn't really have anything to do with tail recursion. 如注释中所示,GHC中的堆栈溢出错误与尾递归实际上没有任何关系。 Instead, it arises from a particular aspect of lazy evaluation. 相反,它来自懒惰评估的特定方面。 (See Do stack overflow errors occur in Haskell? , for example.) (例如,请参阅在Haskell中是否发生堆栈溢出错误 。)

Briefly, GHC creates "thunks" representing unevaluated expressions whose value can be demanded at a future point. 简而言之,GHC会创建代表未评估表达式的“ thunk”,其将来可能需要其值。 Sometimes, these thunks get linked together in a long chain in such a way that when the value of the thunk at one end of the chain is needed, all the thunks down the chain need to be "partially evaluated" to the end of the chain to get the last thunk's value before the program can start calculating thunk values all the way back up the chain. 有时,这些thunk在长链中以这样的方式链接在一起:当需要链的一端的thunk的值时,需要对链中的所有thunk进行“部分评估”,直到链的末端在程序可以开始一直计算链的值之前,获得最后一个链的值。 GHC maintains all these "thunk evaluations in process" in a stack with finite size, and that can overflow. GHC在有限的堆栈中维护所有这些“正在处理的大容量评估”,并且可能会溢出。

A simple example to trigger a stack overflow is: 一个简单的触发堆栈溢出的例子是:

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

If you run this with: 如果使用以下命令运行此命令:

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

you'll get: 你会得到:

Sum.hs: stack overflow

However, compiling it with ghc -O2 is enough to make the problem go away (both for Sum.hs and in your original program). 但是,使用ghc -O2编译足以解决问题(对于Sum.hs以及您的原始程序均如此)。 One of the reasons is likely the application of a "strictness analysis" optimization that forces thunks early so these long chains can't form. 原因之一可能是应用了“严格度分析”优化,该优化可迫使早期的重击,从而无法形成这些长链。

With respect to your second question (is an immutable array the right approach): 关于您的第二个问题(不可变数组是正确的方法):

As @WillNess points out, using unboxed arrays in place of boxed arrays gives a huge performance improvement: on my laptop, an unboxed version of your code runs in 8 secs versus 63 secs. 正如@WillNess指出的那样,使用未装箱的数组代替装箱的数组可以极大地提高性能:在我的笔记本电脑上,未装箱的代码版本运行时间为8秒而不是63秒。

However, with this type of algorithm -- basically, where a large number of small changes are made incrementally to a vector in such a way that the changes to be made depend on the whole history of accumulated previous changes -- you can do much better with a mutable array. 但是,使用这种类型的算法-基本上,对向量进行大量细微更改的方式是,要进行的更改取决于累积的先前更改的整个历史记录-您可以做得更好具有可变数组。 I have a version using Data.Vector.Unboxed.Mutable that runs part 2 in 0.12 secs, and you should be able to achieve similar performance with a mutable unboxed array from Data.Array.Unboxed . 我有一个使用Data.Vector.Unboxed.Mutable的版本在0.12秒内运行第2部分,并且您应该能够通过Data.Array.Unboxed的可变的未装箱数组实现类似的性能。

If you import Data.Array.Unboxed instead of Data.Array , and declare your array as 如果导入Data.Array.Unboxed而不是Data.Array ,并将数组声明为

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

or something, in getData , you'll get significant speedup, bordering on miraculous. 或类似的东西,在getData ,您将获得极大的提速,接近奇迹。

Also, you will have to re-implement lengthArr = rangeSize . bounds 同样,您将不得不重新实现lengthArr = rangeSize . bounds lengthArr = rangeSize . bounds , so it works for the unboxed arrays. lengthArr = rangeSize . bounds ,因此适用于未装箱的数组。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM