Ухудшение производительности во время выполнения для C FFI Callback, когда включены pthreads - PullRequest
2 голосов
/ 18 января 2012

Мне любопытно поведение среды выполнения GHC с опцией threaded в случае, когда C FFI вызывает функцию Haskell. Я написал код для измерения накладных расходов на базовый обратный вызов функции (ниже). Хотя издержки обратного вызова функции уже обсуждались ранее , мне любопытно резкое увеличение общего времени, которое я наблюдал, когда многопоточность включена в коде C (даже когда общее количество вызовов функций в Haskell остается неизменным ). В моем тесте я вызывал функцию Haskell f 5M раз, используя два сценария (GHC 7.0.4, RHEL, 12-ядерный блок, параметры времени выполнения ниже после кода):

  • Одна нить в функции C create_threads: вызов f 5M раз - Общее время 1,32

  • 5 потоков в функции C create_threads: каждый поток вызывает f 1M раз - итак, общее количество еще 5M - общее время 7,79 с

Код ниже - приведенный ниже код на 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, gcc 4.4.3, если она используется ghc):

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

Запуск с 1 потоком в create_threads (код, приведенный выше, сделает это) - я отключил параллельный 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)

Я буду признателен за понимание того, почему производительность ухудшается с несколькими pthreads в create_threads. Сначала я подозревал параллельный сборщик мусора, но отключил его для тестирования выше. Время MUT также резко возрастает для нескольких потоков, учитывая одинаковые параметры времени выполнения. Так что это не просто GC.

Кроме того, есть ли улучшения в GHC 7.4.1 для этого вида сценария?

Я не планирую перезванивать Haskell из FFI так часто, но это помогает понять вышеуказанную проблему при разработке взаимодействия многопоточных библиотек Haskell / C.

1 Ответ

1 голос
/ 18 января 2012

Я полагаю, что ключевой вопрос здесь заключается в том, как график выполнения GHC вызывает обратные вызовы C в Haskell? Хотя я не знаю наверняка, я подозреваю, что все обратные вызовы C обрабатываются потоком Haskell, который первоначально сделал внешний вызов, по крайней мере до ghc-7.2.1 (который я использую).

Это объяснило бы большое замедление, которое вы (и я) видите при переходе от 1 потока к 5. Если все пять потоков обращаются обратно в один и тот же поток Haskell, в этом потоке Haskell возникнет серьезная конкуренция за завершение всех обратные вызовы.

Чтобы проверить это, я изменил ваш код так, что Haskell разветвляет новый поток перед вызовом create_threads, а create_threads порождает только один поток за вызов. Если я не ошибаюсь, каждый поток ОС будет иметь отдельный поток Haskell для выполнения работы, поэтому должно быть гораздо меньше конфликтов. Хотя это все еще занимает почти вдвое больше времени, чем однопоточная версия, она значительно быстрее, чем оригинальная многопоточная версия, что дает некоторые доказательства этой теории. Разница будет намного меньше, если я отключу миграцию потоков с помощью +RTS -qm.

Поскольку Даниэль Фишер сообщает о других результатах для ghc-7.2.2, я ожидаю, что эта версия изменит то, как Haskell планирует обратные вызовы. Может быть, кто-то из списка ghc-users сможет предоставить дополнительную информацию по этому вопросу; Я не вижу ничего вероятного в примечаниях к выпуску 7.2.2 или 7.4.1.

...