web-dev-qa-db-ja.com

FSharpは私のアルゴリズムをPython

数年前、私は動的計画法によって問題を解決しました。

https://www.thanassis.space/fillupDVD.html

ソリューションはPythonでコーディングされました。

視野を広げる一環として、私は最近OCaml/F#を学び始めました。 Python to F#-で記述した命令型コードを直接移植し、そこから始めて関数型プログラミングソリューションに向けて段階的に進めるよりも、水域をテストするためのより良い方法はありません。

この最初の直接ポートの結果は...当惑させられます:

Pythonの場合:

  bash$ time python fitToSize.py
  ....
  real    0m1.482s
  user    0m1.413s
  sys     0m0.067s

FSharpの下:

  bash$ time mono ./fitToSize.exe
  ....
  real    0m2.235s
  user    0m2.427s
  sys     0m0.063s

(上記の「モノラル」に気付いた場合:私はWindowsでも、Visual Studioを使用して同じ速度でテストしました)。

控えめに言っても、私は...困惑しています。 Python F#よりも高速にコードを実行しますか?.NETランタイムを使用してコンパイルされたバイナリは、Pythonの解釈されたコードよりも低速で実行されますか?!?!

VM(この場合はmono)の起動コストと、JITがPythonなどの言語の改善方法については知っていますが、それでも...速度を落とすのではなく、速度を上げることを期待していました。

何か間違ったことをしたことがありますか?

ここにコードをアップロードしました:

https://www.thanassis.space/fsharp.slower.than.python.tar.gz

F#コードは、多かれ少なかれ、Pythonコードの行ごとの直接変換であることに注意してください。

P.S.もちろん、他の利点もあります。 F#によって提供される静的型の安全性-しかし、命令型アルゴリズムの結果の速度がF#の下で悪化する場合...控えめに言っても、私は失望しています。

[〜#〜]編集[〜#〜]:コメントで要求された直接アクセス:

Pythonコード: https://Gist.github.com/950697

fSharpコード: https://Gist.github.com/950699

40
ttsiodras

私が電子メールで連絡したジョン・ハロップ博士は、何が起こっているのかを説明しました。

問題は、プログラムがPython用に最適化されていることです。もちろん、これは、プログラマーが一方の言語をもう一方の言語よりもよく知っている場合によく見られます。 F#プログラムを最適化する方法を指示する別のルールセットを学ぶ必要があります...「fori」ではなく「fori in 1..ndo」ループの使用など、いくつかのことが私に飛びつきました。 = 1 to n do "ループ(一般的には高速ですが、ここでは重要ではありません)、リストに対してList.mapiを繰り返し実行して、配列インデックス(中間リストを不必要に割り当てます)を模倣し、F#TryGetValue for Dictionaryを使用して割り当てます不必要に(参照を受け入れる.NET TryGetValueは一般的に高速ですが、ここではそれほど高速ではありません)

...しかし、本当のキラー問題は、ハッシュテーブルを使用して高密度の2Dマトリックスを実装することであることが判明しました。ハッシュテーブルの使用はPythonで理想的です。これは、ハッシュテーブルの実装が非常によく最適化されているためです(Pythonコードが同じくらい高速に実行されているという事実からも明らかです)。 F#はネイティブコードにコンパイルされています!)ただし、特にデフォルト値をゼロにする場合は、配列が密行列を表すためのはるかに優れた方法です。

面白いのは、このアルゴリズムを最初にコーディングしたとき、私は[〜#〜] did [〜#〜]テーブルを使用しました-理由で実装を辞書に変更しました明確さ(配列境界チェックを回避することでコードが単純になり、推論がはるかに簡単になりました)。

Jonは私のコード(戻る:-))を 配列バージョン に変換し、100倍の速度で実行されます。

この話の教訓:

  • F#ディクショナリには作業が必要です...タプルをキーとして使用する場合、コンパイルされたF#は解釈されたPythonのハッシュテーブルよりも遅くなります!
  • 明らかですが、繰り返しても害はありません。コードがクリーンであるということは、コードがはるかに遅いことを意味する場合があります。

ありがとう、ジョン-どうもありがとう。

[〜#〜] edit [〜#〜]:辞書を配列に置き換えると、コンパイルされた言語が期待する速度でF#が最終的に実行されるという事実実行します。辞書の速度を修正する必要性を否定するものではありません(MSのF#の人々がこれを読んでいることを願っています)。他のアルゴリズムは辞書/ハッシュに依存しており、配列を使用するように簡単に切り替えることはできません。辞書を使用するたびにプログラムを「インタプリタ速度」に苦しめることは、間違いなくバグです。コメントで言われているように、問題がF#ではなく.NET辞書にある場合、これは.NETのバグであると私は主張します。

EDIT2:最も明確な解決策は、アルゴリズムを配列に切り替える必要がないことです(一部のアルゴリズムは単にそれに従わないでしょう)。この:

let optimalResults = new Dictionary<_,_>()

これに:

let optimalResults = new Dictionary<_,_>(HashIdentity.Structural)

この変更により、F#コードの実行速度が2.7倍になり、最終的にPython(1.6倍高速)を上回ります。奇妙なことに、デフォルトではタプルが実行されます構造比較を使用するため、原則として、キーに対して辞書によって行われる比較は同じです(構造の有無にかかわらず)。Harrop博士は、速度の違いは仮想ディスパッチに起因する可能性があると理論付けています: " AFAIK、.NETは仮想ディスパッチを最適化するためにほとんど何もしません。また、仮想ディスパッチのコストは、プログラムカウンターを予測できない場所にジャンプする「計算された後藤」であり、その結果、分岐予測ロジックを損なうため、最新のハードウェアでは非常に高くなります。ほぼ確実に、CPUパイプライン全体がフラッシュされてリロードされます」

簡単に言えば、Don Symeが示唆しているように( 下の3つの答えを見てください )、「参照型のキーを.NETコレクションと組み合わせて使用​​する場合は構造ハッシュの使用について明示してください」。 (以下のコメントのDr. Harropは、.NETコレクションを使用する場合は常に常に構造比較を使用する必要があると述べています)。

MSのF#チームの皆様、これを自動的に修正する方法がある場合は、実行してください。

47
ttsiodras

Jon Harropが指摘しているように、Dictionary(HashIdentity.Structural)を使用して辞書を作成するだけで、パフォーマンスが大幅に向上します(私のコンピューターでは3倍)。これはほぼ間違いなく、Pythonよりも優れたパフォーマンスを得るために必要な最小限の侵襲的な変更であり、コードを慣用的に(タプルを構造体などに置き換えるのではなく)維持し、Python実装。

8
kvb

編集:私は間違っていました、それは値型と参照型の問題ではありません。他のコメントで説明されているように、パフォーマンスの問題はハッシュ関数に関連していました。興味深い議論があるので、私はここに私の答えを保ちます。私のコードはパフォーマンスの問題を部分的に修正しましたが、これはクリーンで推奨される解決策ではありません。

-

私のコンピューターでは、タプルを構造体に置き換えることで、サンプルを実行しました2倍の速度。つまり、同等のF#コードはPythonコードよりも高速に実行されるはずです。 .NETハッシュテーブルが遅いというコメントには同意しません。Pythonや他の言語の実装と大きな違いはないと思います。また、「コードを1対1に変換することは、より高速であると期待することはできません」に同意しません。F#コードは、通常、ほとんどのタスクでPythonよりも高速です(静的型付けは非常に高速です)コンパイラに役立ちます)。あなたのサンプルでは、​​ほとんどの時間がハッシュテーブルのルックアップに費やされているので、両方の言語がshouldほぼ同じくらい速いと想像するのは公平です。

パフォーマンスの問題はガベージコレクションに関連していると思います(ただし、プロファイラーで確認していません)。ここでタプルの使用が構造体よりも遅くなる可能性がある理由は、SOの質問で説明されています( 。Net 4.0の新しいタプル型が値型ではなく参照型(クラス)である理由(struct) )およびMSDNページ( タプルの構築 ):

それらが参照型である場合、タイトループでタプルの要素を変更すると、大量のガベージが生成される可能性があることを意味します。 [...] F#タプルは参照型でしたが、2つ、場合によっては3つの要素タプルを値型にすると、パフォーマンスの向上を実現できるとチームから感じられました。内部タプルを作成した一部のチームは、シナリオが多数の管理対象オブジェクトの作成に非常に敏感であったため、参照型の代わりに値を使用していました。

もちろん、Jonが別のコメントで述べたように、あなたの例での明らかな最適化は、ハッシュテーブルを配列に置き換えることです。配列は明らかにはるかに高速です(整数インデックス、ハッシュなし、衝突処理なし、再割り当てなし、よりコンパクト)が、これは問題に非常に固有であり、Pythonとのパフォーマンスの違いを説明していません(私の知る限り、Pythonコードは配列ではなくハッシュテーブルを使用しています)。

私の50%のスピードアップを再現するために、ここに完全なコードがあります: http://Pastebin.com/nbYrEi5d

要するに、私はタプルをこのタイプに置き換えました:

_type Tup = {x: int; y: int}
_

また、詳細のように見えますが、List.mapi (fun i x -> (i,x)) fileSizesを囲んでいるループの外に移動する必要があります。 Python enumerateは実際にはリストを割り当てないと思います(したがって、F#でリストを1回だけ割り当てるか、Seqモジュールを使用するか、可変カウンターを使用するのが妥当です) 。

5
Laurent