F#vs OCaml:堆栈溢出
我最近为Python程序员发现了一个关于F#的演示文稿,并在看完之后决定自行实施“蚂蚁拼图”解决方案。
有一只蚂蚁可以在平面网格上四处走动。 蚂蚁可以一次向左,向右,向上或向下移动一个空间。 也就是说,从细胞(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中实现了我的解决方案,并试用了它:
$ ocamlopt -unsafe -rectypes -inline 1000 -o puzzle ant.ml
$ time ./puzzle
Points: 148848
real 0m0.143s
user 0m0.127s
sys 0m0.013s
整洁,我的结果与leonardo在D和C ++中实现的结果是一样的。 与leonardo的C ++实现相比,OCaml版本的运行速度比C ++慢大约2倍。 这是可以的,因为莱昂纳多使用队列来消除递归。
然后我将代码翻译成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
令人惊讶的是,它运行在单声道2.10.5(即没有堆栈溢出) - 但它需要84秒,即比OCaml-oops慢587倍。
所以这个程序...
为什么?
编辑:古怪继续 - 使用“--optimize + --checked-”使问题消失, 但只在ArchLinux / Mono下 ; 在Windows XP和Windows 7 / 64bit下,即使是二进制堆栈的优化版本也会溢出。
最终编辑 :我自己发现了答案 - 见下文。
执行摘要:
现在是时候转移到F#。
然后我发布到堆栈溢出 - 但有些人决定关闭这个问题(叹气)。
现在是检查堆栈大小的时候了:在Windows下,另一个SO帖子指出它默认设置为1MB。 在Linux下,“uname -s”和一个测试程序汇编清楚地表明它是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(很好,对于这个简单的小问题很重要)。
无论如何,要彻底解决这个问题,我写了一个尾递归版本 - 函数末尾的递归调用被转换为循环(因此,至少在理论上不需要使用堆栈)。
新版本在Windows下运行良好,并在0.5秒内完成。
所以,故事的道德:
PS Jon Harrop博士的一些额外意见:
......你真幸运,OCaml也没有溢出。 您已经确定实际堆栈大小因平台而异。 同样问题的另一个方面是,不同的语言实现以不同的速率吃掉堆栈空间,并且在存在深度堆栈的情况下具有不同的性能特征。 OCaml,Mono和.NET都使用不同的数据表示和GC算法来影响这些结果......(a)OCaml使用带标记的整数来区分指针,给出紧凑的堆栈帧,并遍历堆栈中的所有内容以查找指针。 标记本质上为OCaml运行时提供了足够的信息以便能够遍历堆(b)Mono将栈中的单词作为指针保守地处理:如果作为指针,一个单词将指向堆分配块,那么块被认为是可达的。 (c)我不知道.NET的算法,但是如果它更快地读取堆栈空间并且仍然遍历堆栈中的每个单词(当然,如果不相关的线程具有深度堆栈,它会受到GC的病态性能的影响,我不会感到惊讶)。 ...此外,使用堆分配元组意味着您将快速填充幼儿园代(例如gen0),因此,GC经常会遍历那些深度堆栈...
让我试着总结答案。
有3点要做:
堆栈溢出异常是递归的结果是非常常见的。 如果调用处于尾部位置,编译器可能会识别它并应用尾调优化,因此递归调用不会占用堆栈空间。 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下它仍然会失败,但是通过使可执行文件运行得更快来缓解这个问题。
最后,最终的解决方案是使用尾递归(通过重写代码或通过实现积极的编译器优化); 这是避免递归函数的堆栈溢出问题的好方法。
链接地址: http://www.djcxy.com/p/80551.html上一篇: F# vs OCaml: Stack overflow
下一篇: F# Create Factorial function without recursion, library functions or loops