2012-01-17 20 views
2

在C FFI回调Haskell函数的情况下,我很好奇GHC运行时的行为与threaded选项。我编写了代码来测量基本函数回调的开销(见下文)。尽管之前函数回调开销已经为discussed,但我很好奇在C代码中启用多线程时(即使对Haskell的函数调用总数保持不变),我观察到的总时间急剧增加。在我的测试,我叫哈斯克尔功能f 500万次使用两种方案(GHC 7.0.4,RHEL,12芯盒,下面的代码之后运行选项):启用pthreads时C FFI回调的运行性能下降

  • 用C create_threads功能单一线程:调用f 5M时间 - 总时间用C create_threads功能1.32s

  • 5个线程:每个线程调用f 100万次 - 这样,总还是5M - 低于7.79s

代码总时间 - 哈斯克尔下面的代码是单线程Ç回调 - 评论解释如何更新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); 

吨。 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,GCC 4.4.3在情况下,它是通过使用GHC):

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

create_threads与1个线程运行(上面的代码将做) - I截止平行GC来进行测试:

$ ./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) 

与5个线程(见第一评论中的上述t.hsmain功能运行如何编辑就为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多个并行线程下降。我首先怀疑是平行GC,但我在上面进行了测试。考虑到相同的运行时选项,MUT时间对于多个pthreads也会大幅上升。所以,这不仅仅是GC。

此外,GHC 7.4.1在这种情况下是否有任何改进?

我不打算从FFI经常回调Haskell,但它有助于在设计Haskell/C多线程库交互时了解上述问题。

+1

对于单线程和2.58s(经过1.86s)的总线时间1.42s(经过1.42s),使用4个线程(因为我只有2个物理内核和4个线程,我认为这是毫无意义的要求五个线程)。所以在7.4.1中可能会更好。 – 2012-01-17 23:02:17

+0

@DanielFischer,感谢7.2.2性能指针。可能是我应该在RHEL上下载并编译7.4.1RC以查看它是如何执行的。尽管这是相当耗时的工作。 – Sal 2012-01-17 23:10:48

+0

我相信他们也有预编译的二进制文件,也适用于发布候选版本。我认为这不会太耗时。或者不要在RHEL上使用vanilla的二进制文件? – 2012-01-17 23:14:17

回答

1

我相信这里的关键问题是,GHC运行时间表C如何回调Haskell?虽然我不确定,但我怀疑所有的C回调都是由最初由外部调用的Haskell线程来处理的,至少是ghc-7.2.1(我正在使用它)。

这将解释您从(从一个线程移动到5)时看到的大幅放缓。如果五个线程都回调到同一个Haskell线程中,那么Haskell线程将会出现重大争用来完成所有回调。

为了测试这个,我修改了你的代码,以便Haskell在调用create_threads之前分出一个新的线程,而create_threads每个调用只产生一个线程。如果我是正确的,每个操作系统线程都会有一个专用的Haskell线程来执行工作,所以应该有更少的争用。虽然这仍然是单线程版本的将近两倍,但它比原始的多线程版本要快得多,这为该理论提供了一些证据。如果我使用+RTS -qm关闭线程迁移,则差异会小得多。

由于Daniel Fischer报告了ghc-7.2.2的不同结果,我预计版本会改变Haskell调度回调的方式。也许ghc-users列表上的某个人可以提供更多信息;我在7.2.2或7.4.1的发行说明中看不到任何可能的东西。

+0

感谢您的反馈意见。你的理论看起来很合理。似乎有某种争用正在进行。我也怀疑回调是单线程的。你所描述的符合观察。我昨天还通过电子邮件发送了ghc用户名单。 – Sal 2012-01-18 12:45:01

+0

在我的测试中验证了您的观察结果。如果将每个pthread映射到一个用于回调的Haskell线程(在7.0.4中),运行时就会很好地扩展。将您的解决方案标记为答案。 – Sal 2012-01-20 12:52:39