我最近在我的OCaml程序中遇到Stack_overflow
错误。如果我打开回溯,我看到异常是由“原始操作”“pervasives.ml”引发的,第270行。我进入OCaml源代码并看到第270行定义了函数@
(即列表附加)。我没有从回溯中获得任何其他信息,即使在我的程序中抛出异常也没有。我切换到字节码并尝试ocamldebug
,但没有帮助(没有生成回溯)。
我认为这是一个非常奇怪的情况。在我的程序中我使用列表的唯一地方是(a)构建包含整数1到1000000的列表,(b)按顺序遍历RBT并将结果放入列表中,以及(c)打印整数列表包含表面上1000000的数字。我已经测试了所有函数,并且它们都没有包含无限循环,我认为1000000甚至不是一个巨大的数字。此外,我已经在Haskell(GHC),Scala和SML(MLton)中尝试了相当于我的程序,并且所有这些版本在相当短的时间内完美地工作。那么,问题是,可能会发生什么?我可以调试吗?
答案 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
库或任何扩展标准库(基本上是电池或基站):所有这些库都重新实现了列表函数的尾递归版本。