F#vs OCaml:堆栈溢出

时间:2011-09-24 10:57:25

标签: f# ocaml stack-overflow tail-recursion

我最近发现了关于F# for Python programmers的演示文稿,在观看之后,决定自己实施“蚂蚁拼图”的解决方案。

有一只蚂蚁可以在平面网格上走动。蚂蚁可以一次向左,向右,向上或向下移动一个空间。也就是说,从单元格(x,y),蚂蚁可以进入单元格(x + 1,y),(x-1,y),(x,y + 1)和(x,y-1)。蚂蚁无法访问x和y坐标的数字之和大于25的点。例如,点(59,79)是不可访问的,因为5 + 9 + 7 + 9 = 30,大于25.问题是:如果从(1000,1000)开始,蚂蚁可以访问多少个点,包括(1000,1000)本身?

我在30行OCaml first中实施了我的解决方案,并尝试了它:

$ ocamlopt -unsafe -rectypes -inline 1000 -o puzzle ant.ml
$ time ./puzzle
Points: 148848

real    0m0.143s
user    0m0.127s
sys     0m0.013s

干净,我的结果与leonardo's implementation, in D and C++的结果相同。与leonardo的C ++实现相比,OCaml版本的运行速度比C ++慢大约2倍。考虑到leonardo使用队列去除递归,这没关系。

我然后translated the code to F# ......这就是我得到的:

Thanassis@HOME /g/Tmp/ant.fsharp
$ /g/Program\ Files/FSharp-2.0.0.0/bin/fsc.exe ant.fs
Microsoft (R) F# 2.0 Compiler build 2.0.0.0
Copyright (c) Microsoft Corporation. All Rights Reserved.

Thanassis@HOME /g/Tmp/ant.fsharp
$ ./ant.exe

Process is terminated due to StackOverflowException.
Quit

Thanassis@HOME /g/Tmp/ant.fsharp
$ /g/Program\ Files/Microsoft\ F#/v4.0/Fsc.exe ant.fs
Microsoft (R) F# 2.0 Compiler build 4.0.30319.1
Copyright (c) Microsoft Corporation. All Rights Reserved.

Thanassis@HOME /g/Tmp/ant.fsharp
$ ./ant.exe

Process is terminated due to StackOverflowException

堆栈溢出...我的机器中有两个版本的F#我... 出于好奇,我接着生成了二进制文件(ant.exe)并在Arch Linux / Mono下运行它:

$ mono -V | head -1
Mono JIT compiler version 2.10.5 (tarball Fri Sep  9 06:34:36 UTC 2011)

$ time mono ./ant.exe
Points: 148848

real    1m24.298s
user    0m0.567s
sys     0m0.027s

令人惊讶的是,它在Mono 2.10.5下运行(即没有堆栈溢出) - 但它需要84秒,即比OCaml慢587倍 - oops。

所以这个程序......

  • 在OCaml下正常运行
  • 在.NET / F#
  • 下根本不起作用
  • 工作,但很慢,在Mono / F#下。

为什么?

编辑:怪异继续 - 使用“--optimize + --checked-”会使问题消失,只会在ArchLinux / Mono 下;在Windows XP和Windows 7 / 64bit下,即使二进制堆栈的优化版本也会溢出。

最终编辑:我自己找到了答案 - 见下文。

2 个答案:

答案 0 :(得分:72)

执行摘要:

  • 我写了一个算法的简单实现......它不是尾递归的。
  • 我在Linux下用OCaml编译了它。
  • 工作正常,在0.14秒内完成。

是时候移植到F#。

  • 我将代码(直接翻译)翻译成F#。
  • 我在Windows下编译并运行它 - 我有一个堆栈溢出。
  • 我在Linux下使用二进制文件,并在Mono下运行。
  • 虽然有效,但运行速度非常慢(84秒)。

然后我发布了Stack Overflow - 但有些人决定关闭这个问题(叹气)。

  • 我尝试使用--optimize + --checked -
  • 进行编译
  • 二进制仍然堆栈在Windows下溢出...
  • ...但在Linux / Mono下运行正常(并在0.5秒内完成)。

是时候检查堆栈大小了:在Windows下,another SO post pointed out that it is set by default to 1MB。在Linux下," uname -s" a compilation of a test program清楚地表明它是8MB。

这解释了为什么程序在Linux下运行而不在Windows下运行(该程序使用超过1MB的堆栈)。它没有解释为什么优化版本在Mono下比非优化版本运行得更好:0.5秒vs 84秒(即使-optimize +似乎默认设置,请参阅Keith的评论&# 34;专家F#"摘录)。可能与Mono的垃圾收集器有关,它在某种程度上被第一版驱动到了极端。

Linux / OCaml和Linux / Mono / F#执行时间(0.14 vs 0.5)之间的区别是因为我测量它的简单方法:" time。/ binary ..."测量启动时间,这对于Mono / .NET来说很重要(对于这个简单的小问题,这很重要)。

无论如何,要一劳永逸地解决这个问题,我wrote a tail-recursive version - 函数末尾的递归调用转换为循环(因此,不需要堆栈使用 - 至少在理论上)

新版本在Windows下运行良好,并在0.5秒内完成。

所以,故事的道德:

  • 请注意您的堆栈使用情况,特别是如果您使用大量堆栈并在Windows下运行。使用EDITBIN with the /STACK option将二进制文件设置为更大的堆栈大小,或者更好的是,以不依赖于使用过多堆栈的方式编写代码。
  • OCaml在消除尾部递归方面可能比F#更好 - 或者它的垃圾收集器在这个特定问题上做得更好。
  • 不要对...粗暴的人关闭Stack Overflow问题感到绝望,好人会最终抵制他们 - 如果问题真的很好: - )

<强> P.S。 Jon Harrop博士的一些额外意见:

...你很幸运,OCaml也没有溢出。 您已经确定实际堆栈大小因平台而异。 同一问题的另一个方面是不同的语言实现 以不同的速度吃堆栈空间,并有不同的表现 存在深层堆栈时的特征。 OCaml,Mono和.NET 所有都使用不同的数据表示和影响的GC算法 这些结果......(a)OCaml使用标记整数来区分指针, 给出紧凑的堆栈帧,并将遍历堆栈中的所有内容 寻找指针。标记基本上传达了足够的信息 因为OCaml运行时能够遍历堆(b)Mono处理单词 在堆栈上保守地作为指针:如果作为指针,一个单词会指向 在堆分配的块中,该块被认为是可达的。 (c)我不知道.NET的算法,但如果它吃了堆栈我不会感到惊讶 空间更快,仍然遍历堆栈中的每个字(当然 如果不相关的线程有一个,则会从GC中受到病理性能的影响 深堆栈!)...此外,您使用堆分配的元组意味着您将会这样做 快速填充托儿所(例如gen0),因此, 导致GC经常遍历那些深层堆栈......

答案 1 :(得分:8)

让我试着总结一下答案。

有三点要做:

  • 问题:递归函数发生堆栈溢出
  • 它只发生在windows下:在linux上,对于检查的问题大小,它可以正常工作
  • OCaml中的相同(或类似)代码
  • optimize + compiler flag,对于检查的问题大小,可以正常工作

Stack Overflow异常是递归vall的结果,这是很常见的。如果调用处于尾部位置,则编译器可以识别它并应用尾调优化,因此递归调用不会占用堆栈空间。 Tailcall优化可能发生在F#,CRL或两者中:

CLR尾部优化1

F#递归(更一般)2

F#尾调用3

正如其他所说,“在Windows上失败,而不是在Linux中失败”的正确解释是两个OS上的默认保留堆栈空间。或者更好的是,两个操作系统下编译器使用的保留堆栈空间。默认情况下,VC ++仅保留1MB的堆栈空间。 CLR(可能)是用VC ++编译的,所以它有这个限制。保留的堆栈空间可以在编译时增加,但我不确定它是否可以在已编译的可执行文件上进行修改。

编辑:事实证明它可以完成(参见此博文http://www.bluebytesoftware.com/blog/2006/07/04/ModifyingStackReserveAndCommitSizesOnExistingBinaries.aspx) 我不会推荐它,但在极端情况下,至少它是可能的。

OCaml版本可能有效,因为它是在Linux下运行的。 但是,在Windows下测试OCaml版本也会很有趣。我知道OCaml编译器在尾部调用优化方面比F#更具攻击性。它甚至可以从原始代码中提取尾部可重用函数吗?

我对“--optimize +”的猜测是它仍然会导致代码重复出现,因此它仍会在Windows下失败,但会通过使可执行文件运行得更快来缓解这个问题。

最后,最终的解决方案是使用尾递归(通过重写代码或通过重新分析积极的编译器优化);这是一种避免使用递归函数的堆栈溢出问题的好方法。