web-dev-qa-db-ja.com

Haskellに暗黙の並列処理がないのはなぜですか?

Haskellは機能的で純粋なので、基本的にコンパイラが取り組むことができるようにするために必要なすべてのプロパティを備えています 暗黙の並列処理

この些細な例を考えてみましょう。

f = do
  a <- Just 1
  b <- Just $ Just 2
  -- ^ The above line does not utilize an `a` variable, so it can be safely
  -- executed in parallel with the preceding line
  c <- b
  -- ^ The above line references a `b` variable, so it can only be executed
  -- sequentially after it
  return (a, c)
  -- On the exit from a monad scope we wait for all computations to finish and 
  -- gather the results

概略的には、実行プランは次のように説明できます。

               do
                |
      +---------+---------+
      |                   |
  a <- Just 1      b <- Just $ Just 2
      |                   |
      |                 c <- b
      |                   |
      +---------+---------+
                |
           return (a, c)

フラグまたはプラグマを使用してコンパイラにそのような機能がまだ実装されていないのはなぜですか?実用的な理由は何ですか?

57
Nikita Volkov

これは長い間研究されてきたトピックです。 Haskellコードで暗黙的に並列処理を導出することはできますが、問題は、現在のハードウェアでは並列処理が多すぎて、きめが細かすぎることです。

そのため、物事をより速く実行するのではなく、簿記に労力を費やすことになります。

無限の並列ハードウェアがないため、適切な粒度を選択することがすべてです。粗すぎるとアイドル状態のプロセッサが存在し、細かすぎてオーバーヘッドが許容できなくなります。

私たちが持っているのは、数千または数百万の並列タスクを生成するのに適した、より粗い並列処理(スパーク)です(命令レベルではありません)。これは、今日通常利用できるほんの一握りのコアにマッピングされます。

一部のサブセット(配列処理など)には、厳密なコストモデルを備えた完全自動の並列化ライブラリがあることに注意してください。

この背景については、 http://research.Microsoft.com/en-us/um/people/tharris/papers/2007-fdip.pdf を参照してください。ここでは、挿入への自動化されたアプローチが紹介されています。任意のHaskellプログラムでのparの。

76
Don Stewart

abの間の暗黙的なデータ依存性のため、コードブロックは最良の例ではないかもしれませんが、これら2つのバインディングがその中で通勤することは注目に値します。

f = do
  a <- Just 1
  b <- Just $ Just 2
  ...

同じ結果が得られます

f = do
  b <- Just $ Just 2
  a <- Just 1
  ...

したがって、これは投機的な方法で並列化することができます。これはモナドとは何の関係も必要ないことは注目に値します。たとえば、letブロック内のすべての独立した式を並行して評価したり、そうするバージョンのletを導入したりできます。 lparallel library for CommonLISPがこれを行います。

今、私はこの問題の専門家ではありませんが、これが問題の私の理解です。主な障害は、複数の式の評価を並列化することが有利な場合を判断することです。評価のために個別のスレッドを開始することに関連するオーバーヘッドがあり、あなたの例が示すように、それは無駄な作業をもたらす可能性があります。一部の式は小さすぎて、オーバーヘッドに見合う並列評価を行うことができない場合があります。私が理解しているように、式のコストの完全に正確なメトリックは停止問題を解決することになります。そのため、並行して評価するものを決定するためにヒューリスティックなアプローチを使用することになります。

その場合、問題に対してより多くのコアをスローする方が常に速いとは限りません。利用可能な多くのHaskellライブラリで問題を明示的に並列化する場合でも、メモリの割り当てと使用量が多く、ガベージコレクタとCPUキャッシュに負担がかかるため、式を並列に評価するだけではあまり高速化されないことがよくあります。最終的には、コンパクトなメモリレイアウトが必要になり、データをインテリジェントにトラバースする必要があります。リンクリストを16スレッドでトラバースすると、メモリバスでボトルネックになり、実際に処理が遅くなる可能性があります。

少なくとも、効果的に並列化できる式は、多くのプログラマーには明らかではないため(少なくとも、これはそうではありません)、コンパイラーに効果的に並列化させることは簡単ではありません。

24
sabauma

簡単な答え:ものを並行して実行すると、速くなるのではなく、遅くなることがあります。そして、それがいつであり、いつそれが良い考えではないかを理解することは、未解決の研究問題です。

ただし、「スレッド、デッドロック、競合状態を気にすることなく、これらすべてのコアを突然利用する」ことはできます。自動ではありません。コンパイラにそれを行う場所についてのヒントを与える必要があります! :-D

7

その理由の1つは、Haskellが厳密ではなく、デフォルトでは何も評価しないためです。一般に、コンパイラはabの計算が終了することを知らないため、計算しようとするとリソースが無駄になります。

x :: Maybe ([Int], [Int])
x = Just undefined
y :: Maybe ([Int], [Int])
y = Just (undefined, undefined)
z :: Maybe ([Int], [Int])
z = Just ([0], [1..])
a :: Maybe ([Int], [Int])
a = undefined
b :: Maybe ([Int], [Int])
b = Just ([0], map fib [0..])
    where fib 0 = 1
          fib 1 = 1
          fib n = fib (n - 1) + fib (n - 2)

以下の機能について検討してください。

main1 x = case x of
              Just _ -> putStrLn "Just"
              Nothing -> putStrLn "Nothing"

(a, b)パーツを評価する必要はありません。 x = Just _を取得するとすぐに、分岐に進むことができます。したがって、a以外のすべての値で機能します。

main2 x = case x of
              Just (_, _) -> putStrLn "Just"
              Nothing -> putStrLn "Nothing"

この関数は、タプルの評価を強制します。したがって、xはエラーで終了しますが、残りは機能します。

main3 x = case x of
              Just (a, b) -> print a >> print b
              Nothing -> putStrLn "Nothing"

この関数は、最初に最初のリストを印刷し、次に2番目に印刷します。 zで機能します(結果として無限の数のストリームが出力されますが、Haskellはそれを処理できます)。 bは最終的にメモリが不足します。

これで、一般的に、計算が終了するかどうか、および計算が消費するリソースの数はわかりません。 Haskellでは無限のリストは完全に問題ありません:

main = maybe (return ()) (print . take 5 . snd) b -- Prints first 5 Fibbonacci numbers

したがって、Haskellで式を評価するためにスレッドを生成すると、完全に評価されることを意図していないもの(たとえば、すべての素数のリスト)を評価しようとする可能性がありますが、プログラマーは構造の一部として使用します。上記の例は非常に単純で、コンパイラがそれらに気付く可能性があると主張するかもしれません-しかし、停止問題のために一般的には不可能です(任意のプログラムとその入力を受け取り、それが終了するかどうかを確認するプログラムを書くことはできません)-したがって、そうではありません安全な最適化。

さらに、他の回答で言及されているように、追加のスレッドのオーバーヘッドが関与する価値があるかどうかを予測することは困難です。 GHCはグリーンスレッドを使用してスパーク用の新しいスレッドを生成しませんが(カーネルスレッドの数は固定されています-いくつかの例外はあります)、データを1つのコアから別のコアに移動し、それらの間で同期する必要があります。これは非常にコストがかかる可能性があります。

ただし、Haskellは、parや同様の関数によって言語の純粋さを損なうことなく、並列化をガイドしています。

4

実際にはそのような試みがありましたが、利用可能なコアの数が少ないため、一般的なハードウェアではありませんでした。プロジェクトは Reduceron と呼ばれます。高レベルの並列処理でHaskellコードを実行します。 適切な2 GHz ASICコア としてリリースされた場合、Haskellの実行速度に重大な突破口があります。

2
polkovnikov.ph