OCaml:pervasives.ml中的Stack_overflow异常

时间:2018-03-12 09:37:40

标签: debugging ocaml stack-overflow

我最近在我的OCaml程序中遇到Stack_overflow错误。如果我打开回溯,我看到异常是由“原始操作”“pervasives.ml”引发的,第270行。我进入OCaml源代码并看到第270行定义了函数@(即列表附加)。我没有从回溯中获得任何其他信息,即使在我的程序中抛出异常也没有。我切换到字节码并尝试ocamldebug,但没有帮助(没有生成回溯)。

我认为这是一个非常奇怪的情况。在我的程序中我使用列表的唯一地方是(a)构建包含整数1到1000000的列表,(b)按顺序遍历RBT并将结果放入列表中,以及(c)打印整数列表包含表面上1000000的数字。我已经测试了所有函数,并且它们都没有包含无限循环,我认为1000000甚至不是一个巨大的数字。此外,我已经在Haskell(GHC),Scala和SML(MLton)中尝试了相当于我的程序,并且所有这些版本在相当短的时间内完美地工作。那么,问题是,可能会发生什么?我可以调试吗?

2 个答案:

答案 0 :(得分:5)

@运算符在OCaml标准库中不是尾递归的,

let rec ( @ ) l1 l2 =
  match l1 with
    [] -> l2
  | hd :: tl -> hd :: (tl @ l2)

因此,使用大型列表(作为左参数)调用它将溢出堆栈。

您可以通过在已生成的列表的末尾添加新元素来构建列表,例如,

let rec init n x = if n > 0 then init (n-1) x @ [x] else []

这具有时间复杂度n^2并且将消耗堆栈空间中的n个插槽。

关于一般性问题 - 如何调试这种堆栈溢出,我通常的方法是减少堆栈大小,以便在跟踪膨胀之前尽快触发问题,例如,

OCAMLRUNPARAM=b,l=1024 ocaml ./test.ml

如果您要将OCaml代码编译为本机代码,则需要将-g选项传递给编译器,以便它可以生成回溯。此外,在本机执行中,堆栈的大小由操作系统控制,应使用操作系统的相应机制进行设置,例如在GNU / Linux中使用ulimit,例如ulimit -s 1024

作为奖励跟踪,以下init函数是尾递归的,并且具有O(N)时间复杂度并将占用O(1)堆栈空间:

let init n x =
  let rec loop n xs =
    if n = 0 then xs else loop (n-1) (x :: xs) in
  loop n []

这个想法是使用累加器列表并在堆空间中构建列表。

如果您不喜欢考虑尾递归,那么您可以使用Janestreet Base库(或Core)或Batteries库。它们都提供init函数的尾递归版本,并保证所有其他函数都是尾递归的。

答案 1 :(得分:0)

标准库中的列表函数针对小列表进行了优化,并不一定是尾递归的;部分证明列表不是用于存储大量数据的有效数据结构(请注意,Haskell列表是惰性的,因此与OCaml eager列表完全不同)。

特别是,如果使用@得到堆栈溢出错误,由于@的复杂度在大小上是线性的,因此很可能实现了具有二次时间复杂度的算法它的左参数。

它们可能是比你的问题列表更好的数据结构,如果你想要迭代,序列库或任何其他形式的迭代器将会更有效率。

通过前面提到的所有警告,重新定义标准库函数的尾递归但低效的版本是相对简单的,例如, :

 let (@!) x y = List.rev_append (List.rev x) y

另一种选择是使用containers库或任何扩展标准库(基本上是电池或基站):所有这些库都重新实现了列表函数的尾递归版本。