OCaml:Stack_overflow pervasives.ml 中的异常

OCaml: Stack_overflow exception in pervasives.ml

最近我的 OCaml 程序出现 Stack_overflow 错误。如果我打开回溯,我会看到第 270 行 "primitive operation" "pervasives.ml" 引发了异常。我进入 OCaml 源代码并看到第 270 行定义了函数 @(即列表追加)。我没有从回溯中获得任何其他信息,甚至在我的程序中抛出异常的地方也没有。我切换到字节码并尝试 ocamldebug,但它没有帮助(没有生成回溯)。

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

标准库中的列表函数针对小列表进行了优化,不一定tail-recursive;部分理由是列表不是存储大量数据的有效数据结构(请注意 Haskell 列表是惰性的,因此与 OCaml 热切列表完全不同)。

特别是,如果您在使用 @ 时遇到计算器溢出错误,您很可能正在实施具有二次 time-complexity 的算法,因为 @ 的复杂度是与其左参数的大小成线性关系。

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

考虑到前面提到的所有警告,重新定义 tail-recursive 但标准库函数的低效版本相对简单,例如:

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

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

OCaml标准库中@运算符不是tail-recursive,

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 个槽位 space。

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

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

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

作为奖励曲目,以下 init 函数是尾递归函数,具有 O(N) 时间复杂度,需要 O(1) 个堆栈 space:

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

想法是使用累加器列表并在堆中构建列表 space。

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