web-dev-qa-db-ja.com

F#とOCaml:スタックオーバーフロー

最近、私は 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倍遅くなります。

したがって、このプログラム...

  • oCamlで問題なく動作する
  • .NET/F#ではまったく機能しません
  • 動作しますが、Mono/F#では非常に遅くなります。

どうして?

編集:奇妙さが続く-「--optimize + --checked-」を使用すると問題が消えるしかしArchLinux/Mono; Windows XPおよびWindows 7/64ビットでは、バイナリスタックの最適化されたバージョンでもオーバーフローします。

最終編集:私は自分で答えを見つけました-以下を参照してください。

63
ttsiodras

エグゼクティブサマリー:

  • 私はアルゴリズムの簡単な実装を書きました...それは末尾再帰ではありませんでした。
  • LinuxでOCamlを使用してコンパイルしました。
  • うまくいき、0.14秒で終了しました。

その後、F#に移植するときがきました。

  • コードをF#に翻訳(直接翻訳)しました。
  • Windowsでコンパイルして実行しました-スタックオーバーフローが発生しました。
  • Linuxでバイナリを取得し、Monoで実行しました。
  • 動作しましたが、非常にゆっくり実行されました(84秒)。

次にStack Overflowに投稿しましたが、一部の人が質問を閉じることにしました(ため息)。

  • --optimize + --checked-でコンパイルしてみました
  • バイナリがまだWindowsでオーバーフローしています...
  • ...しかし、Linux/Monoでは問題なく実行され(0.5秒で終了)、.

スタックサイズを確認する時が来ました: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秒で終了しました。

だから、物語の教訓:

  • スタックの使用量に注意してください。特に、大量に使用してWindowsで実行する場合は注意してください。 / STACKオプションを指定したEDITBIN を使用して、バイナリをより大きなスタックサイズに設定するか、より多くのスタックの使用に依存しない方法でコードを記述します。
  • OCamlは、F#よりも末尾再帰の排除に優れている可能性があります-または、この特定の問題では、ガベージコレクターの方が優れています。
  • スタックオーバーフローの質問を閉じる失礼な人々について絶望しないでください。善良な人々は結局、彼らに対抗します-質問が本当に良い場合:-)

P.S。ジョンハーロップ博士からの追加入力:

... OCamlもオーバーフローしなかったのは幸運でした。実際のスタックサイズはプラットフォーム間で異なることをすでに確認しました。同じ問題のもう1つの側面は、異なる言語実装が異なる速度でスタックスペースを消費し、深いスタックが存在する場合に異なるパフォーマンス特性を持つことです。 OCaml、Mono、.NETはすべて、これらの結果に影響を与えるさまざまなデータ表現とGCアルゴリズムを使用します...(a)OCamlはタグ付き整数を使用してポインターを区別し、コンパクトなスタックフレームを提供し、ポインターを探してスタック上のすべてをトラバースします。タグ付けは基本的に、OCamlランタイムがヒープをトラバースできるようにするのに十分な情報を伝えます(b)Monoはスタック上の単語を控えめにポインターとして扱います:ポインターとして、Wordがヒープに割り当てられたブロックを指す場合、ブロックは到達可能と見なされます。 (c)私は.NETのアルゴリズムを知りませんが、スタックスペースをより速く食べ、スタック上のすべてのWordをトラバースしても驚かないでしょう(無関係なスレッドが深いスタックを持っている場合、GCの病理学的パフォーマンスに確実に影響します!) ...さらに、ヒープに割り当てられたタプルを使用すると、ナーサリ世代(たとえばgen0)がすぐにいっぱいになるため、GCがこれらの深いスタックを頻繁にトラバースすることになります...

72
ttsiodras

答えを要約してみましょう。

3つのポイントを作成する必要があります。

  • 問題:再帰関数でスタックオーバーフローが発生する
  • これはWindowsでのみ発生します。Linuxでは、問題のサイズが調査されたため、機能します
  • oCamlの同じ(または同様の)コードが機能する
  • 検査された問題のあるサイズに対して、optimize +コンパイラフラグが機能する

スタックオーバーフロー例外が再帰的バルの結果であることが非常に一般的です。呼び出しが末尾の位置にある場合、コンパイラはそれを認識し、末尾呼び出しの最適化を適用する可能性があるため、再帰呼び出しはスタック領域を占有しません。 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では失敗しますが、実行可能ファイルの実行を高速化することで問題を軽減します。

最後に、決定的な解決策は、(コードを書き直すか、積極的なコンパイラの最適化を実現することによって)末尾再帰を使用することです。再帰関数でスタックオーバーフローの問題を回避するための良い方法です。

8