【问题标题】:F# vs OCaml: Stack overflowF# vs OCaml:堆栈溢出
【发布时间】:2011-11-24 05:06:26
【问题描述】:

我最近发现了一个关于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) 本身?

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

$ 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 倍 - 哎呀。

所以这个程序...

  • 在 OCaml 下运行良好
  • 在 .NET/F# 下根本不起作用
  • 在 Mono/F# 下可以工作,但速度很慢。

为什么?

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

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

【问题讨论】:

  • @CarstenKönig Expert F# 2.0 说:“...使用 --optimize 编译您的最终代码,它对您的代码应用最大优化。这也是 fsc.exe 的默认优化设置。”在 pg164 上。这是否意味着 ttsiodras 已经启用了所有优化?
  • 对 walk 的递归调用不在尾部位置。这就解释了为什么会出现堆栈溢出。为什么在其他情况下不会出现堆栈溢出,可能是因为在这些系统上堆栈设置得更大,或者是因为 mono JIT 和 OCAML 编译器做了一些非常聪明的事情。
  • 我认为这是一个很好的问题,但它被关闭太糟糕了。对“请明确说明您要问的内容”的顺序的初步评论将解决此问题。或者,至少它应该在 ttsiodras 修复之后重新打开。
  • 我同意这是一个非常有趣的问题,并且正在投票重新打开它。关于这个主题可以说的比这个评论的空间允许的要多得多......
  • 听起来你找到了答案。为什么要赏金?还有什么问题?

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


【解决方案1】:

执行摘要:

  • 我写了一个算法的简单实现...不是尾递归的。
  • 我在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 的“Expert 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 问题感到绝望,好人最终会抵消它们 - 如果问题真的很好:-)

附注Jon Harrop 博士的一些补充意见:

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

【讨论】:

    【解决方案2】:

    让我试着总结一下答案。

    有3点需要说明:

    • 问题:递归函数发生堆栈溢出
    • 它只发生在 windows 下:在 linux 上,对于检查的问题大小,它可以工作
    • OCaml 中相同(或相似)的代码工作
    • optimize+ 编译器标志,适用于检查的问题大小

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

    CLR尾部优化1

    F# 递归(更通用)2

    F# 尾调用3

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

    编辑:原来是可以做到的(见这篇博文http://www.bluebytesoftware.com/blog/2006/07/04/ModifyingStackReserveAndCommitSizesOnExistingBinaries.aspx) 我不会推荐它,但在极端情况下至少是可能的。

    OCaml 版本可能工作,因为它是在 Linux 下运行的。 但是,在 Windows 下测试 OCaml 版本会很有趣。我知道 OCaml 编译器在尾调用优化方面比 F# 更积极。它甚至可以从您的原始代码中提取尾递归函数吗?

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

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

    【讨论】:

    • “OCaml 编译器在尾调用优化方面比 F# 更积极”。并不真地。 OCaml 和 F# 都保证尾部位置的所有调用都被消除(在适当的情况下)。然而,OCaml 使用的堆栈帧可能比 .NET 或 Mono 小得多。
    猜你喜欢
    • 2017-09-29
    • 1970-01-01
    • 2016-05-30
    • 1970-01-01
    • 2010-11-11
    • 2019-05-18
    • 1970-01-01
    • 1970-01-01
    • 2015-03-14
    相关资源
    最近更新 更多