2012-01-17 10 views
2

C FFIがHaskell関数をコールバックした場合に、threadedオプションを使用してGHCランタイムの動作が不思議です。私は基本的な関数コールバックのオーバーヘッドを測定するコードを書いています(下記)。関数コールバックのオーバーヘッドはすでにdiscussedでしたが、Haskellへの関数呼び出しの総数が同じであっても、Cコードでマルチスレッドが有効になったときに観測された合計時間が急激に増加しているのが不思議です。私のテストでは、私はHaskellの関数と呼ばれる2つのシナリオ(GHC 7.0.4、RHEL、12コアボックス、コードの後に​​以下のランタイム・オプション)を使用してf 5M回:C create_threads機能でpthreadが有効になっているときにC FFIコールバックの実行時パフォーマンスが低下する

  • シングルスレッド:コールをf 5M時間 - 合計時間C create_threads関数で1.32s

  • 5スレッド:各スレッドの呼び出しf 1M時間 - ので、合計は依然として5Mである - 7.79s

コード以下合計時間 - ハスケル以下のコードは、シングルスレッドCコールバックのためのものである - コメントは5スレッド試験のためにそれを更新する方法について説明:

t.hs:

{-# LANGUAGE BangPatterns #-} 
import qualified Data.Vector.Storable as SV 
import Control.Monad (mapM, mapM_) 
import Foreign.Ptr (Ptr, FunPtr, freeHaskellFunPtr) 
import Foreign.C.Types (CInt) 

f :: CInt ->() 
f x =() 

-- "wrapper" import is a converter for converting a Haskell function to a foreign function pointer 
foreign import ccall "wrapper" 
    wrap :: (CInt ->()) -> IO (FunPtr (CInt ->())) 

foreign import ccall safe "mt.h create_threads" 
    createThreads :: Ptr (FunPtr (CInt ->())) -> Ptr CInt -> CInt -> IO() 

main = do 
    -- set threads=[1..5], l=1000000 for multi-threaded FFI callback testing 
    let threads = [1..1] 
     l = 5000000 
     vl = SV.replicate (length threads) (fromIntegral l) -- make a vector of l 
    lf <- mapM (\x -> wrap f) threads -- wrap f into a funPtr and create a list 
    let vf = SV.fromList lf -- create vector of FunPtr to f 
    -- pass vector of function pointer to f, and vector of l to create_threads 
    -- create_threads will spawn threads (equal to length of threads list) 
    -- each pthread will call back f l times - then we can check the overhead 
    SV.unsafeWith vf $ \x -> 
    SV.unsafeWith vl $ \y -> createThreads x y (fromIntegral $ SV.length vl) 
    SV.mapM_ freeHaskellFunPtr vf 

mt.h:

#include <pthread.h> 
#include <stdio.h> 

typedef void(*FunctionPtr)(int); 

/** Struct for passing argument to thread 
** 
**/ 
typedef struct threadArgs{ 
    int threadId; 
    FunctionPtr fn; 
    int length; 
} threadArgs; 


/* This is our thread function. It is like main(), but for a thread*/ 
void *threadFunc(void *arg); 
void create_threads(FunctionPtr*,int*,int); 

MT。 C:

#include "mt.h" 


/* This is our thread function. It is like main(), but for a thread*/ 
void *threadFunc(void *arg) 
{ 
    FunctionPtr fn; 
    threadArgs args = *(threadArgs*) arg; 
    int id = args.threadId; 
    int length = args.length; 
    fn = args.fn; 
    int i; 
    for (i=0; i < length;){ 
    fn(i++); //call haskell function 
    } 
} 

void create_threads(FunctionPtr* fp, int* length, int numThreads) 
{ 
    pthread_t pth[numThreads]; // this is our thread identifier 
    threadArgs args[numThreads]; 
    int t; 
    for (t=0; t < numThreads;){ 
    args[t].threadId = t; 
    args[t].fn = *(fp + t); 
    args[t].length = *(length + t); 
    pthread_create(&pth[t],NULL,threadFunc,&args[t]); 
    t++; 
    } 

    for (t=0; t < numThreads;t++){ 
    pthread_join(pth[t],NULL); 
    } 
    printf("All threads terminated\n"); 
} 

コンパイル(GHC 7.0.4、それはGHCで使用する場合には、GCC 4.4.3):

つのスレッドを実行

$ ./t +RTS -s -N5 -g1 
INIT time 0.00s ( 0.00s elapsed) 
    MUT time 1.04s ( 1.05s elapsed) 
    GC time 0.28s ( 0.28s elapsed) 
    EXIT time 0.00s ( 0.00s elapsed) 
    Total time 1.32s ( 1.34s elapsed) 

    %GC time  21.1% (21.2% elapsed) 

(上記t.hsmain関数で最初のコメントを参照してください。私はテストのために、並列GCをオフ - (上記のコードは、それを行います)create_threads 1つのスレッドで実行

$ ghc -O2 t.hs mt.c -lpthread -threaded -rtsopts -optc-O2 

5つのスレッド)のためにそれを編集する方法について:

$ ./t +RTS -s -N5 -g1 
INIT time 0.00s ( 0.00s elapsed) 
    MUT time 7.42s ( 2.27s elapsed) 
    GC time 0.36s ( 0.37s elapsed) 
    EXIT time 0.00s ( 0.00s elapsed) 
    Total time 7.79s ( 2.63s elapsed) 

    %GC time  4.7% (13.9% elapsed) 

私はパフォーマンスがcreate_threadsで複数のpthreadに低下する理由についての洞察に感謝します。最初に並列GCの疑いがありましたが、上記のテストではオフにしました。同じランタイムオプションを指定すると、複数のpthreadに対してMUT時間も急激に増加します。したがって、GCだけではありません。

また、このようなシナリオではGHC 7.4.1の改良点はありますか?

HaskellをFFIから頻繁に呼び戻すつもりはありませんが、Haskell/Cのマルチスレッドライブラリの相互作用を設計する際に、上記の問題を理解するのに役立ちます。

+1

シングルスレッドの合計時間が1.42秒(1.42秒経過)、スレッドが4つの2.58秒(1.86秒経過)の合計時間が7.2.2と非常に小さくなりました(4つのスレッドで2つの物理コアしかないため、私は5つのスレッドを求めるのは無意味だと思った)。だから、おそらく7.4.1で良いでしょう。 –

+0

@DanielFischer、7.2.2のパフォーマンスのポインタに感謝します。 RHELで7.4.1RCをダウンロードしてコンパイルして、それがどのように動作するかを確認する必要があるかもしれません。それはかなり時間のかかる作業です。 – Sal

+0

私は、リリース候補者にもバイナリをあらかじめビルドしていると思います。それはあまり時間がかかりませんと思います。あるいは、バニラバイナリはRHEL上で動作しませんか? –

答えて

1

ここで重要なのは、GHCランタイムスケジュールCがHaskellにどのようにコールバックするのでしょうか?私は確かに分かっていませんが、私の疑問は、Cコールバックは、少なくともghc-7.2.1(これは私が使用しています)まで、外来呼び出しを行ったHaskellスレッドによって処理されることです。

これは、1スレッドから5スレッドに移行するときの大きな減速を説明します。5つのスレッドがすべて同じHaskellスレッドをコールバックしている場合、そのHaskellスレッドにはすべてのコールバックを完了するための大きな競合が発生します。

これをテストするために、Haskellがcreate_threadsを呼び出す前に新しいスレッドをフォークし、create_threadsが呼び出しごとに1つのスレッドしか生成しないようにコードを修正しました。私が正しいとすれば、各OSスレッドは作業を実行するための専用のHaskellスレッドを持つことになるので、競合がはるかに少なくなるはずです。これにはシングルスレッドバージョンの約2倍の時間がかかりますが、元のマルチスレッドバージョンよりも大幅に高速です。この理論にはいくつかの証拠があります。 +RTS -qmでスレッドの移行を無効にすると、その差ははるかに小さくなります。

Daniel Fischerがghc-7.2.2で異なる結果を報告したので、私はHaskellがコールバックをどのようにスケジューリングするかを変更する予定です。おそらく、ghc-usersのリストの誰かがこれに関する詳細情報を提供することができます。 7.2.2または7.4.1のリリースノートには何も表示されません。

+0

フィードバックに感謝します。あなたの理論はとても説得力があります。何らかの種類の競合が起きているようです。私もコールバックがシングルスレッドであると疑っています。あなたが説明したことは観察に合っています。私は昨日ghc-usersリストにメールを送りました。 – Sal

+0

私のテストであなたの観測を検証しました。各pthreadをコールバックのための1つのHaskellスレッド(7.0.4)にマップすると、ランタイムはうまくスケーリングされます。解決策を回答としてマークしてください。 – Sal

関連する問題