在C FFI回调Haskell函数的情况下,我对使用threaded
选项的GHC运行时的行为感到好奇。我编写了代码来测量基本函数回调的开销(下图)。虽然函数回调开销之前已经是discussed,但我很好奇我在C代码中启用多线程时观察到的总时间急剧增加(即使对Haskell的函数调用总数保持不变)。在我的测试中,我使用两个场景(GHC 7.0.4,RHEL,12核盒,代码后面的运行时选项)调用了Haskell函数f
5M次:
C create_threads
函数中的单线程:调用f
5M次 - 总时间1.32s
C create_threads
函数中的5个线程:每个线程调用f
1M次 - 所以,总数仍为5M - 总时间为7.79s
下面的代码 - 下面的Haskell代码用于单线程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):
$ ghc -O2 t.hs mt.c -lpthread -threaded -rtsopts -optc-O2
在create_threads
中运行1个线程(上面的代码会这样做) - 我关闭并行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个线程运行(请参阅上面main
t.hs
函数的第一条评论,了解如何编辑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是否有任何改进?
我不打算经常从FFI回调Haskell,但在设计Haskell / C多线程库交互时,它有助于理解上述问题。
答案 0 :(得分:1)
我认为这里的关键问题是,GHC运行时如何调度C回调到Haskell?虽然我不确定,但我怀疑所有的C回调都是由最初进行外部调用的Haskell线程处理的,至少是ghc-7.2.1(我正在使用)。
这可以解释你(和我)从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的发行说明中没有看到任何可能的内容。