Сравнение потоков Haskell с потоками ядра — жизнеспособен ли мой тест?

#multithreading #haskell #pthreads

#многопоточность #haskell #pthreads

Вопрос:

На самом деле это для моего университетского проекта. В моем эссе мне нужно привести доказательства того, что потоки Haskell создаются быстрее, чем обычные потоки ядра. Я знаю, что лучше обратиться к какой-нибудь исследовательской работе, но дело в том, что я должен провести бенчмаркинг самостоятельно.

Вот что я придумал. Я написал две программы, на C (используя pthreads) и Haskell, которые создают много потоков, но эти потоки абсолютно ничего не делают. Мне нужно измерить только скорость создания потока.

Вот исходный код для программы на C:

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

void* thread_main(void*);

int main(int argc, char* argv[])
{
   int n,i;
    pthread_t *threads;
    pthread_attr_t pthread_custom_attr;

    if (argc != 2)
    {
        printf ("Usage: %s nn  where n is no. of threadsn",argv[0]);
       return 1;
   }

    n=atoi(argv[1]);

    threads=(pthread_t *)malloc(n*sizeof(*threads));
    pthread_attr_init(amp;pthread_custom_attr);

    for (i=0; i<n; i  )
    {
        pthread_create(amp;threads[i], amp;pthread_custom_attr, thread_main, (void *)(0));
    }

    for (i=0; i<n; i  )
    {
        pthread_join(threads[i],NULL);
    }
}

void* thread_main(void* p)
{
   return 0;
}
  

и для программы Haskell:

 module Main (main) where

import System.IO.Unsafe
import System
import Control.Concurrent
import Control.Exception

children :: MVar [MVar ()]
children = unsafePerformIO (newMVar [])

waitForChildren :: IO ()
waitForChildren = do
   cs <- takeMVar children
   case cs of
      []   -> return ()
      m:ms -> do
         putMVar children ms
         takeMVar m
         waitForChildren

forkChild :: IO () -> IO ThreadId
forkChild io = do
   mvar <- newEmptyMVar
   childs <- takeMVar children
   putMVar children (mvar:childs)
   forkIO (io `finally` putMVar mvar ())

forkKids :: Int -> IO ()
forkKids 0 = return ()
forkKids n = do
   forkChild (threadMain)
   forkKids (n-1)

threadMain = return ()

main = do
   args <- getArgs
   forkKids (read (head args))
   waitForChildren
  

Теперь, что я делаю, я запускаю каждую программу с одним и тем же аргументом (например, 10000) и измеряю время их выполнения с помощью time -f%e , затем беру среднее арифметическое времени выполнения. Это показывает, что создание потоков Haskell на порядок быстрее.

Теперь мой вопрос: правильный ли это тест? или есть какой-то фактор, который мне нужно учитывать, чтобы получить точные результаты?

Спасибо

Комментарии:

1. … каковы были ваши результаты, кстати?

2. Вы можете увидеть результаты здесь: 10098.arnet.am/posts/haskells-lightweight-threads

3. Примечание: вы должны добавить {-# NOINLINE children #-} при использовании unsafePerformIO для создания подобного глобального MVar. В противном случае он может быть встроен, и в итоге вы получите несколько разных MVAR’ов.

Ответ №1:

Ваши бенчмарки, вероятно, дают вам желаемый результат, но там ужасно много шума. Вы измеряете не «сколько времени требуется для создания потока», а «сколько времени требуется для запуска программы, которая создает несколько потоков, а затем ожидает их возврата перед завершением».

Ответы, вероятно, более или менее одинаковы на практике, но при бенчмаркинге вам следует попытаться сузить круг поиска, чтобы сравнить то, что вас интересует, с как можно меньшим внешним шумом.

Почему бы вам просто не установить таймер вокруг вызовов pthread_create / forkIO , поскольку именно их вы хотите измерить?

Вас не интересует, сколько времени требуется для запуска вашей программы, поэтому не рассчитывайте время. Вас не интересует, сколько времени требуется для последующего объединения потоков, поэтому не рассчитывайте это время.

Комментарии:

1. Хотя вы правы в том, что он сказал, что его не интересует, сколько времени требуется для объединения потоков, я думаю, что это также потенциально довольно интересно. Возможно, заслуживает дополнительного примечания в том, что он пишет.

2. Верно, но многие вещи интересны. Если это достаточно интересно для измерения, то его следует измерять должным образом, изолированно (насколько это возможно). То есть учитывайте затраты на создание потока, а затем отдельно учитывайте затраты на объединение потоков.

Ответ №2:

В зависимости от количества потоков pthread_create() в какой-то момент может прекратиться создание потоков; на это следует обратить внимание при тестировании.