提问人:wojand 提问时间:11/13/2023 更新时间:11/15/2023 访问量:54
在 OCaml 中的大型列表文本上编译期间堆栈溢出
Stack overflow during compilation on large list literal in OCaml
问:
我有一个小项目来检查一个数字是否是素数。这个想法是准备(生成)一个素数列表,直到某个点供库函数使用(为此使用代码生成器)。
虽然代码生成工作正常,但如果列表很大(例如,对于最多一百万个素数列表,即 ~75k 个条目),则生成文件的编译会失败。
沙丘建造输出:
dune build 输出的部分(底部)--verbose:
一些代码:
(* prime_list.mli *)
val long_list : int list
(* THIS FAILS TO COMPILE: prime_list.ml (was generated, found only in _build directory) *)
let long_list = [2;3;5;7;
<a lot more entries, depending on parameter provided in dune file...>]
据我了解,列表字面意思是(这当然是糖,因为在编译过程中有太深的嵌套调用和堆栈溢出(可能是解析而不是构造值?可悲的是,我根本不知道编译在 OCaml 中是如何工作的)。2 :: (3 :: ( 5 :: ... )))
::
你有什么办法解决吗?我想要一个列表,只要我愿意,使用尾递归编写每个函数,编译器就这样让我失望了......
回顾一下:我在内存中(在生成器中)有列表,但无法将其转储到文件(最好是 .ml)中,从而允许我在主程序中烘焙它,这令人沮丧。
出于绝望,我也尝试生成如下文件:
(* prime_list.ml, another try *)
let long_list = []
let long_list = 999983 :: long_list
let long_list = 999979 :: long_list
...
let long_list = 3 :: long_list
let long_list = 2 :: long_list
以及
let long_list0 = []
let long_list1 = 999983 :: long_list0
let long_list2 = 999979 :: long_list1
...
let long_list78487 = 3 :: long_list78496
let long_list78498 = 2 :: long_list78497
let long_list = long_list78498
但这两种方法都导致了同样的事情()。我是OCaml的新手,我不明白为什么这不起作用。我对这个问题的答案很感兴趣——可能有点猜测。当然,除了解决我的问题。Fatal error: exception Stack overflow
也许我可以以某种方式更改堆栈的限制?从这个角度(编译器设置和标志)的答案将不胜感激,但这并不能真正令人满意作为最终答案,因为它可能不会无限扩展(不过我可能错了,谁知道呢?
以下是 lib/dune 文件内容,以确保完整性:
; primes_with_generator/lib/dune
(library
(name primes_with_generator)
(libraries commons))
(rule
(target prime_list.ml)
(deps (:gen ../generator/gen.exe))
(action (with-stdout-to %{target} (run %{gen} 1000000))))
1_000_000 是这里的一个参数 -- 生成最多 1_000_000 的素数。对于生成高达 100_000 的素数,一切都很好,但我希望素数高达 sqrt(INT MAX) ≈ 2^32 ≈ 10^9...好吧,至少不想因为这样的愚蠢原因而停在 ~200k。
Commons 可能无关紧要,但这里是它的界面供您查看:
(* commons/prime.mli *)
(* given arguments:
- list of primes up to a point, should be in ascending order, may be empty),
- a number n >= 2
returns answer to the question: is number n prime?
*)
val is_prime : int list -> int -> bool
答:
我尝试定义一个包含 75,000 个 int 的列表,但编译器确实在编译过程中用完了堆栈空间。这很有趣,但我认为必须对程序进行一些限制。
我猜想你对单独构建列表的问题是编译器试图进行不断折叠,即在编译时创建相同的大列表。
通过在运行时进行串联,我能够获得 75,000 个整数的列表。
从本质上讲,我定义了 75 个列表,每个列表有 1000 个整数。然后,我定义了一个类型的值,其中包含每个单独的列表。int list list
let sublist1 = [1; 2; ...; 1000 ]
. . .
let sublist75 = [1; 2; ...; 1000 ]
let list_of_lists = [ sublist1; sublist2; ...; sublist75 ]
最后我有:
let long_list = List.concat list_of_lists
main 函数如下所示:
let main () = Printf.printf "%d\n" (List.length long_list)
let () = main ()
当我运行它时,我看到这个:
$ ./big
75000
评论
快速而肮脏的解决方案是增加堆栈大小(在 linux 上或切换到 OCaml 5)。ulimit -s unlimited
可扩展的解决方案是不混合代码和数据。质数表是数据,应该这样存储。作为一种快速存储格式,您可以使用 Marshal
模块。如果出于某种原因,您确实不想拥有单独的数据文件,则可以将数据存储为字符串:
let primes: int array =
Marshal.from_string
"\132\149\166\190\000\000\000\006\000\000\000\001\000\000\000\006\000\000\000\006\208BCEGK"
0
它应该一直工作到数组太大而无法完全保留在内存中。
编辑:
为了从文件加载数据,您可以使用 which 自行负责打开和关闭通道(即使引发异常)In_channel.with_open_bin
let load filename: int array =
In_channel.with_open_bin filename @@ fun chan ->
Marshal.from_channel chan
评论
Bigarray.Array1
Unix.map_file
这种方法会派上用场)