7.4. Concurrent Haskell を使う

GHC は特別なオプションや専用にコンパイルしたライブラリなしのデフォルトで Concurrent Haskell をサポートしています. Concurrent Haskellのサポートライブラリにアクセスするには単に Control.Concurrent をインポートすればよいだけです. Concurrent Haskell に関する情報については,このモジュールの文書が提供しています.

任意選択で,プログラムを -threaded オプション(Options affecting linking 参照)でリンクできます. これには2つの利点があります.

  • -N ⟨x⟩ を使って,スレッドをマルチプロセッサあるいマルチコアの計算機で動かせます. SMP 並列を使う を参照してください.
  • あるスレッドが外部呼び出しを行うと(その呼び出しに unsafe が付いていなければ), その外部呼び出し中にプログラム中の他のHaskellスレッドが走り続けます. さらに foreign export された Haskell の関数が複数のOSスレッドから同時に呼ばれてもよい. Multi-threading and the FFI を参照

以下の RTS オプションが Concurrent Haskell プログラムの振舞いに影響します.

-C ⟨s⟩
Default:20 ミリ秒

コンテキストスイッチの間隔を ⟨s⟩ 秒に設定する. コンテキストスイッチは,時間切れの後の最初のヒープブロック確保の際に発生します. (ヒープブロックの確保は確保4k毎に発生します.) -C0 または -C を使うと,コンテキストスイッチは可能なかぎり頻繁に起こります (ヒープブロック確保毎になります).

7.5. SMP 並列を使う

GHC はSMP(対称マルチプロセッサ)上でHaskellプログラムが並列に走るのをサポートしています.

並行性並列性 の間には細かい違いがあります. 並列性とは,複数のプロセッサーを同時に使用することで,プログラムの実行速度を向上させることです. 一方,並行処理は抽象化の手段であり,複数の非同期イベントに応答する必要があるプログラムを構造化する便利な方法です.

しかし,この2つの用語は確かに関連しています. 複数のCPUを使用することにより,並行スレッドを並列で実行することが可能になります. これは,まさにGHCのSMP並列処理のサポートと同じです. しかし,並行性を使用しないプログラムでは,並列性を使用してパフォーマンスを向上させることも可能です. このセクションでは,GHCを使用して並列プログラムをコンパイルおよび実行する方法について説明します. Concurrent and Parallel Haskell では並列性に影響を与える言語機能について説明します.

7.5.1. SMP 並列のためのコンパイルオプション

複数のCPUを使うためには,プログラムは -threaded オプション(Options affecting linking 参照)で リンクしなければなりません. さらに,次のコンパイルオプションが並列性に影響します.

-feager-blackholing

ブラックホール化は,評価中のサンク(遅延計算)をマークする行為です. 第1にある種の無限ループ(NonTermination 例外)を検出させ, 第2にある種の空間リークを回避し, 第3に並列プログラムで計算の反復を避けられます. 計算が進行中であるタイミングを教えられるからです.

-feager-blackholing オプションは,評価が始まるとすぐに,各サンクをブラックホール化します. デフォルトでは,「遅延ブラックホール化」で,スレッドが何らかの理由で一時停止している場合に, 評価中のサンクであることを示すマークだけを付けます. 遅延ブラックホール化は通常より効率的(およぼ1-2%程度)です. ほとんどのサンクはブラックホール化の必要がないからです. しかし,先行ブラックホール化を使えば,並列プログラム中の何度も行われる計算を避けることができます. そして多くの場合並列性にとって重要であることが判ります.

並列に走らせたいコードはどれも -feager-blackholing フラグを付けてコンパイルすることを推奨します.

7.5.2. SMP 並列のための RTS オプション

マルチプロセッサ上でプログラムを走らせる方法は2とおりあります. プログラムから Control.Concurrent.setNumCapabilities を呼ぶ方法と, RTSオプション -N ⟨x⟩ を使う方法です.

-N ⟨x⟩
-maxN ⟨x⟩

プログラムを走らせる際, ⟨x⟩本のスレッドを同時に使います.

実行時システムは仮想プロセッサ一式を管理します. ケーパビリティ と呼ぶ仮想プロセッサの数は -N オプションで指定します. 各ケーパビリティは一度に1つのHaskellスレッドを走らせられます. したがって,ケーパビリティ数は物理的に並列に走れるHaskellスレッドの数と等しくなります. 1つのケーパビリティは1つ以上のOSスレッドによって起動されます. 実行時システムは各ケーパビリティに対応するOSスレッドプールを管理します. Haskellスレッドが外部呼び出し(Multi-threading and the FFI 参照)をすると, 別のOSスレッドがこのケーパビリティを引き継げるようになっています.

通常⟨x⟩はマシン上のCPUコアの数と一致させるべきです [1] . たとえば,デュアルコアマシンであれば +RTS -N2 -RTS を使うことになるでしょう.

⟨x⟩を省略して +RTS -N -RTS のように書くと,実行時システムがマシン上のプロセッサ数にもとづいて⟨x⟩の値を決めます.

-maxN ⟨x⟩ を使って +RTS -maxN3 -RTS のように書くと,実行時システムは 高々⟨x⟩の値に決めることになります.この値の上限はシステム上のプロセッサ数です. ⟨x⟩を省略するとエラーになります.デフォルトでの値が必要なら -N オプションを使って下さい.

マシン内のすべてのプロセッサを使用する場合は注意が必要です. 一部のプロセッサが他のプログラムで使用されている場合,パフォーマンスは向上せず,パフォーマンスに悪影響を与えます. どのような場合でも,GHCに物理スレッドよりも多くのケーパビリティを作成させるのは良くない考えです.

-N を設定すると,並列ガーベッジコレクタ(RTS options to control the garbage collector 参照)も有効になります.

現在の -N オプションの値は,Haskellのプログラム内から, Control.Concurrent.getNumCapabilities を通じて利用可能で Control.Concurrent.setNumCapabilities を呼べば,プログラムを走らせながら変更することも可能です.

以下のオプションを指定すると実行時システムがCPUにスレッドを割り振る方法に影響を与えられます.

-qa

OS アフィニティ機能を使って OS スレッドを CPUコアにピン留めします.

このオプションが有効なら,ケーパビリティ \(i\) に対応する OS のスレッドは OS が適用する API でスレッドアフィニティ(Linunx 用の GHC なら sched_setaffinity() を使って)を設定すれば, CPUのコア \(i\) に束縛できます.

ワークロードやマシン上の他のアクティビティーによって,パフォーマンスが向上する場合もあれば, そうでない場合もあります.実際に試して、違いを測定することをお勧めします.

-qm

負荷分散のための自動マイグレーションを無効にします. 通常実行時システムは,ひまなCPUを活用するためにCPUをまたいでスレッドをスケジュールしようとします. このオプションはこの振る舞いを無効にします. マイグレーションはスレッドにしか適用されないことに注意してください. par によって作られたスパークは,別途 work-stealing で負荷分散されます.

このオプションが役に立つのは,並行プログラムで Control.Concurrent.forkOn を使ってスレッドを 明示的にCPUにスケジュールする場合だけでしょう.

7.5.3. SMP 並列を使うためのヒント

プログラムの実行時に -s [⟨file⟩] という RTS オプションを付けると, 時間統計情報を見られます.これを使えば,使用するCPU数を増やしたことでプログラムが速くなったかを確認できます. ユーザ(user)時間が消費時間(elapsed)時間よりも大きいなら,プログラムは複数のCPUを使ったことになります. 比較のために -N ⟨x⟩ なしでプログラムを走らせてみるのもよいでしょう.

+RTS -s による出力を見れば,プログラム実行中にいくつの「スパーク」(sparks)が作られ,実行されたかが判ります (RTS options to control the garbage collector 参照). そうすれば par がどの程度うまく働らいているかの感触が得られることでしょう.

多くの実験と実行時システムのチューニングにより,GHC の並列性サポートは 6.12.1 で改善されました. この機能がどの程度上手くいっているかについては引き続き,教えていただきたいと思います. また,ベンチマーク用の並列プログラムも集めたいと考えています.

[1]ハイパースレッディングのコアを数に含めるめきかどうかは未解決の問題です. 遠慮なく実験して結果を知らせてください.