Haskell 启用pthread时C FFI回调的运行时性能下降

Haskell 启用pthread时C FFI回调的运行时性能下降,haskell,concurrency,ffi,Haskell,Concurrency,Ffi,我对GHC运行时在C FFI调用Haskell函数时使用threaded选项的行为感到好奇。我编写代码来测量基本函数回调的开销(见下文)。虽然函数回调开销以前就已经存在,但我很好奇,在C代码中启用多线程时(即使对Haskell的函数调用总数保持不变),我观察到的总时间会急剧增加。在我的测试中,我使用两种场景调用了Haskell函数f5M次(GHC 7.0.4,RHEL,12核盒,代码后面的运行时选项): C中的单线程create_线程函数:调用f5M次-总时间1.32s Ccreate_th

我对GHC运行时在C FFI调用Haskell函数时使用
threaded
选项的行为感到好奇。我编写代码来测量基本函数回调的开销(见下文)。虽然函数回调开销以前就已经存在,但我很好奇,在C代码中启用多线程时(即使对Haskell的函数调用总数保持不变),我观察到的总时间会急剧增加。在我的测试中,我使用两种场景调用了Haskell函数
f
5M次(GHC 7.0.4,RHEL,12核盒,代码后面的运行时选项):

  • C中的单线程
    create_线程
    函数:调用
    f
    5M次-总时间1.32s

  • C
    create_threads
    function中的5个线程:每个线程调用
    f
    1M次-因此,总数仍然是5M-总时间7.79s

下面的代码-下面的Haskell代码用于单线程C回调-注释解释了如何为5线程测试更新它:

t、 房协:

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个线程运行(请参阅上文
t.hs
main
函数中关于如何编辑5个线程的第一条注释):

我希望能够深入了解为什么在create_线程中使用多个pthread会降低性能。我首先怀疑是并行GC,但在上面的测试中我关闭了它。给定相同的运行时选项,多个pthread的MUT时间也会急剧增加。所以,这不仅仅是GC

此外,对于这种情况,GHC 7.4.1中是否有任何改进


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

我认为这里的关键问题是,GHC运行时如何安排C回调到Haskell?虽然我不确定,但我怀疑所有的C回调都是由最初进行外部调用的Haskell线程处理的,至少在ghc-7.2.1之前(我正在使用它)

这就解释了你(和我)在从1个线程移动到5个线程时所看到的巨大减速。如果这五个线程都回调到同一个Haskell线程中,那么在该Haskell线程上将有大量的争用来完成所有回调

为了测试这一点,我修改了您的代码,以便Haskell在调用
create\u threads
之前分叉一个新线程,并且
create\u threads
每次调用只生成一个线程。如果我是正确的,每个操作系统线程将有一个专用的Haskell线程来执行工作,因此争用应该会少很多。尽管这仍然是单线程版本的两倍,但它比最初的多线程版本要快得多,这为这一理论提供了一些证据。如果我使用
+RTS-qm
关闭线程迁移,那么差别就会小得多


由于Daniel Fischer报告了ghc-7.2.2的不同结果,我希望版本会改变Haskell安排回调的方式。也许名单上的人可以提供更多的信息;我在7.2.2或7.4.1的发行说明中没有看到任何可能的结果。

我在7.2.2中得到了小得多的减速,单线程的总时间为1.42s(1.42s),而四线程的总时间为2.58s(1.86s)(因为我只有两个物理内核和四个线程,我认为要求五个线程是没有意义的)。因此,在7.4.1中可能会更好。@DanielFischer,感谢您对7.2.2性能的介绍。也许我应该在RHEL上下载并编译7.4.1RC,看看它的性能如何。不过,这是相当耗时的工作。我相信他们已经为候选版本预先构建了二进制文件。我想那不会太费时。或者香草二进制文件在RHEL上不起作用吗?@DanielFischer,香草二进制文件在RHEL5上不起作用,因为它的glibc版本比编译二进制文件的版本旧。谢谢您的反馈。你的理论看起来很有道理。似乎有某种争论在进行。我也怀疑回调是单线程的。你所描述的符合观察结果。我昨天还通过电子邮件向ghc用户列表发送了邮件。在我的测试中验证了您的观察结果。如果我将每个pthread映射到一个Haskell线程进行回调(在7.0.4中),那么运行时的伸缩性很好。将您的解决方案标记为答案。
#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);
#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 -O2 t.hs mt.c -lpthread -threaded -rtsopts -optc-O2
$ ./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 +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)