最近、私は F#for Python programmers に関するプレゼンテーションを見つけました。それを見て、自分で「ant puzzle」の解決策を実装することにしました。
平面グリッド上を歩き回れるアリがいます。アリは一度に1スペース左、右、上または下に移動できます。つまり、セル(x、y)から、アリはセル(x + 1、y)、(x-1、y)、(x、y + 1)、および(x、y-1)に移動できます。 x座標とy座標の数字の合計が25より大きい点には、アリはアクセスできません。たとえば、5 + 9 + 7 + 9 = 30であるので、ポイント(59,79)にはアクセスできません。これは、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
驚いたことに、Mono 2.10.5(スタックオーバーフローなし)で動作しますが、84秒かかります。つまり、OCamlより587倍遅くなります。
したがって、このプログラム...
どうして?
編集:奇妙さが続く-「--optimize + --checked-」を使用すると問題が消えるしかしArchLinux/Mono; Windows XPおよびWindows 7/64ビットでは、バイナリスタックの最適化されたバージョンでもオーバーフローします。
最終編集:私は自分で答えを見つけました-以下を参照してください。
エグゼクティブサマリー:
その後、F#に移植するときがきました。
次にStack Overflowに投稿しましたが、一部の人が質問を閉じることにしました(ため息)。
スタックサイズを確認する時が来ました:Windowsの場合 another SO postは、デフォルトで1MBに設定されていることを指摘しました 。Linuxの場合、「uname -s」および テストプログラムのコンパイル は、8MBであることを明確に示しています。
これは、プログラムがWindowsではなくLinuxで動作する理由を説明しました(プログラムは1MBを超えるスタックを使用しました)。最適化されたバージョンが、最適化されていないバージョンよりもMonoの方がはるかに優れている理由は説明されていません:0.5秒vs 84秒(--optimize +はデフォルトで設定されているようですが、Keithの「Expert F#」のコメントを参照してください)エキス)。たぶん、最初のバージョンでどうにか極端になったMonoのガベージコレクターと関係があります。
Linux/OCamlとLinux/Mono/F#の実行時間の違い(0.14対0.5)は、私が測定した単純な方法によるものです。「time ./binary ...」は、起動時間も測定します。これは、Monoにとって重要です。 /.NET(まあ、この単純な小さな問題にとって重要です)。
とにかく、これを一度に解決するために、私は 末尾再帰バージョンを書きました -関数の最後の再帰呼び出しがループに変換されるため、スタックを使用する必要はありません-少なくとも理論的には)。
新しいバージョンはWindowsでも正常に動作し、0.5秒で終了しました。
だから、物語の教訓:
P.S。ジョンハーロップ博士からの追加入力:
... OCamlもオーバーフローしなかったのは幸運でした。実際のスタックサイズはプラットフォーム間で異なることをすでに確認しました。同じ問題のもう1つの側面は、異なる言語実装が異なる速度でスタックスペースを消費し、深いスタックが存在する場合に異なるパフォーマンス特性を持つことです。 OCaml、Mono、.NETはすべて、これらの結果に影響を与えるさまざまなデータ表現とGCアルゴリズムを使用します...(a)OCamlはタグ付き整数を使用してポインターを区別し、コンパクトなスタックフレームを提供し、ポインターを探してスタック上のすべてをトラバースします。タグ付けは基本的に、OCamlランタイムがヒープをトラバースできるようにするのに十分な情報を伝えます(b)Monoはスタック上の単語を控えめにポインターとして扱います:ポインターとして、Wordがヒープに割り当てられたブロックを指す場合、ブロックは到達可能と見なされます。 (c)私は.NETのアルゴリズムを知りませんが、スタックスペースをより速く食べ、スタック上のすべてのWordをトラバースしても驚かないでしょう(無関係なスレッドが深いスタックを持っている場合、GCの病理学的パフォーマンスに確実に影響します!) ...さらに、ヒープに割り当てられたタプルを使用すると、ナーサリ世代(たとえばgen0)がすぐにいっぱいになるため、GCがこれらの深いスタックを頻繁にトラバースすることになります...
答えを要約してみましょう。
3つのポイントを作成する必要があります。
スタックオーバーフロー例外が再帰的バルの結果であることが非常に一般的です。呼び出しが末尾の位置にある場合、コンパイラはそれを認識し、末尾呼び出しの最適化を適用する可能性があるため、再帰呼び出しはスタック領域を占有しません。 Tailcallの最適化は、F#、CRL、またはその両方で発生する可能性があります。
CLRテール最適化 1
F#再帰(より一般的) 2
F#テールコール
「LinuxではなくWindowsで失敗する」の正しい説明は、別の言い方をすれば、2つのOSのデフォルトの予約済みスタックスペースです。または、2つのOSでコンパイラが使用する予約済みスタックスペース。デフォルトでは、VC++は1MBのスタックスペースのみを予約します。 CLRは(おそらく)VC++でコンパイルされるため、この制限があります。予約済みのスタックスペースはコンパイル時に増やすことができますが、コンパイルされた実行可能ファイルで変更できるかどうかはわかりません。
編集:それができることがわかります(このブログ投稿を参照してください http://www.bluebytesoftware.com/blog/2006/07/04/ModifyingStackReserveAndCommitSizesOnExistingBinaries.aspx )私はそれをお勧めしませんが、極端な状況では、少なくともそれは可能です。
Linuxで実行されたOCamlバージョンは動作する可能性があります。ただし、WindowsでOCamlバージョンもテストすることは興味深いでしょう。 OCamlコンパイラーは、F#よりもテールコールの最適化に積極的であることを知っています。元のコードからテールコール可能な関数を抽出することもできますか?
"--optimize +"についての私の推測では、コードが繰り返し発生するため、Windowsでは失敗しますが、実行可能ファイルの実行を高速化することで問題を軽減します。
最後に、決定的な解決策は、(コードを書き直すか、積極的なコンパイラの最適化を実現することによって)末尾再帰を使用することです。再帰関数でスタックオーバーフローの問題を回避するための良い方法です。