Eratosthenes的筛选Pthread实现:线程数不影响计算时间

时间:2016-05-24 07:29:18

标签: c multithreading parallel-processing pthreads sieve-of-eratosthenes

我正在尝试用Pthread实现并行的Eratosthenes程序筛选。我已完成编码并且程序正常工作并且符合预期,这意味着如果我使用多于1个线程,则计算时间将小于顺序程序(仅使用1个线程)。但是,无论我使用多少额外的线程,计算时间基本相同。例如,如果我从1到10亿进行计算,则顺序程序使用大约21秒,而使用2个线程的并行程序使用大约14秒。但是当我尝试使用3,4,5,10,20,50个线程时,它总是需要大约14秒。我想知道导致这个问题的原因以及解决方法。我的代码如下:

#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
//The group of arguments passed to thread
struct thrd_data{
  long id;
  long start;
  long end; /* the sub-range is from start to end */
};
typedef struct {
  pthread_mutex_t     count_lock;     /* mutex semaphore for the barrier */
  pthread_cond_t      ok_to_proceed;  /* condition variable for leaving */
  long                count;      /* count of the number of threads who have  arrived */
} mylib_barrier_t;

//global variable
bool *GlobalList;//The list of nature number
long Num_Threads;
mylib_barrier_t barrier;/* barrier */

void mylib_barrier_init(mylib_barrier_t *b)
{
  b -> count = 0;
  pthread_mutex_init(&(b -> count_lock), NULL);
  pthread_cond_init(&(b -> ok_to_proceed), NULL);
}

void mylib_barrier(mylib_barrier_t *b, long id) 
{
   pthread_mutex_lock(&(b -> count_lock));
   b -> count ++;
   if (b -> count == Num_Threads)
   {
     b -> count = 0; /* must be reset for future re-use */
     pthread_cond_broadcast(&(b -> ok_to_proceed));
   }
   else
   {
    while (pthread_cond_wait(&(b -> ok_to_proceed), &(b -> count_lock)) !=    0);

    }
    pthread_mutex_unlock(&(b -> count_lock));
}

void mylib_barrier_destroy(mylib_barrier_t *b) 
{
  pthread_mutex_destroy(&(b -> count_lock));
  pthread_cond_destroy(&(b -> ok_to_proceed));
}

void *DoSieve(void *thrd_arg)
{

  struct thrd_data *t_data;
  long i,start, end;
  long k=2;//The current prime number in first loop
  long myid;

  /* Initialize my part of the global array */
  t_data = (struct thrd_data *) thrd_arg;
  myid = t_data->id;
  start = t_data->start;
  end = t_data->end;

  printf ("Thread %ld doing look-up from %ld to %ld\n", myid,start,end);
  //First loop: find all prime numbers that's less than sqrt(n)
  while (k*k<=end) 
  {
      int flag;
      if(k*k>=start)
        flag=0;
      else
        flag=1;
      //Second loop: mark all multiples of current prime number
      for (i = !flag? k*k-1:start+k-start%k-1; i <= end; i += k)
        GlobalList[i] = 1;
      i=k;
      //wait for other threads to finish the second loop for current prime   number
      mylib_barrier(&barrier,myid);
      //find next prime number that's greater than current one
      while (GlobalList[i] == 1)
            i++;
         k = i+1;

   }
  //decrement the counter of threads before exit
  pthread_mutex_lock (&barrier.count_lock);
  Num_Threads--;
  if (barrier.count == Num_Threads)
  {
    barrier.count = 0;  /* must be reset for future re-use */
    pthread_cond_broadcast(&(barrier.ok_to_proceed));
  }
  pthread_mutex_unlock (&barrier.count_lock);
  pthread_exit(NULL);
}


int main(int argc, char *argv[])
{
  long i, n,n_threads;
  long k, nq, nr;
  FILE *results;
  struct thrd_data *t_arg;
  pthread_t *thread_id;
  pthread_attr_t attr;

  /* Pthreads setup: initialize barrier and explicitly create
   threads in a joinable state (for portability)  */
  mylib_barrier_init(&barrier);
  pthread_attr_init(&attr);
  pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_JOINABLE);

  /* ask to enter n and n_threads from the user */
  printf ("enter the range n = ");
  scanf ("%ld", &n);
  printf ("enter the number of threads n_threads = ");
  scanf ("%ld", &n_threads);
  time_t start = time(0);//set initial time
  //Initialize global list
  GlobalList=(bool *)malloc(sizeof(bool)*n);
  for(i=0;i<n;i++)
    GlobalList[i]=0;
  /* create arrays of thread ids and thread args */
  thread_id = (pthread_t *)malloc(sizeof(pthread_t)*n_threads);
  t_arg = (struct thrd_data *)malloc(sizeof(struct thrd_data)*n_threads);

  /* distribute load and create threads for computation */
  nq = n / n_threads;
  nr = n % n_threads;

  k = 1;
  Num_Threads=n_threads;
  for (i=0; i<n_threads; i++){
    t_arg[i].id = i;
    t_arg[i].start = k;
    if (i < nr)
        k = k + nq + 1;
    else
        k = k + nq;
    t_arg[i].end = k-1;
    pthread_create(&thread_id[i], &attr, DoSieve, (void *) &t_arg[i]);
  }

  /* Wait for all threads to complete then print all prime numbers */
  for (i=0; i<n_threads; i++) {
    pthread_join(thread_id[i], NULL);
  }
  int j=1;
  //Get the spent time for the computation works by all participanting threads
  time_t stop = time(0);
  printf("Time to do everything except print = %lu seconds\n", (unsigned   long)    (stop-start));
  //print the result of prime numbers
  printf("The prime numbers are listed below:\n");
  for (i = 1; i < n; i++)
  {
    if (GlobalList[i] == 0)
    {
        printf("%ld ", i + 1);
        j++;
    }
    if (j% 15 == 0)
        printf("\n");
  }
  printf("\n");
  // Clean up and exit 
  free(GlobalList);
  pthread_attr_destroy(&attr);
  mylib_barrier_destroy(&barrier); // destroy barrier object
  pthread_exit (NULL);
}

2 个答案:

答案 0 :(得分:1)

你做了有效的观察。更多线程并不意味着要完成更多工作。

您正在双核CPU上运行程序。你已经用2个线程使系统饱和了。

只有1个线程将使用1个核心。使用2个线程将使用2个核心。假设有4个线程,您将看到与2个线程相同的性能。超线程无济于事,因为逻辑核心(HT核心)与其物理核心共享内存系统。

这是运行

的输出
  

perf stat -d筛子

      23879.553188      task-clock (msec)         #    1.191 CPUs utilized          
             3,666      context-switches          #    0.154 K/sec                  
             1,470      cpu-migrations            #    0.062 K/sec                  
           219,177      page-faults               #    0.009 M/sec                  
    76,070,790,848      cycles                    #    3.186 GHz                    
   <not supported>      stalled-cycles-frontend  
   <not supported>      stalled-cycles-backend   
    34,500,622,236      instructions              #    0.45  insns per cycle        
     4,172,395,541      branches                  #  174.727 M/sec                  
         1,020,010      branch-misses             #    0.02% of all branches        
    21,065,385,093      L1-dcache-loads           #  882.152 M/sec                  
     1,223,920,596      L1-dcache-load-misses     #    5.81% of all L1-dcache hits  
        69,357,488      LLC-loads                 #    2.904 M/sec                  
   <not supported>      LLC-load-misses:HG  

这是i5-4460 CPU硬件性能监视器的输出。它跟踪一些有趣的统计数据。

请注意每个循环计数的低指令。 cpu每个周期执行0.45条指令。通常你想看到这个值&gt; 1。

更新:需要注意的关键是增加线程数无济于事。 CPU只能进行有限数量的分支和内存访问。

答案 1 :(得分:-1)

两个观察结果。

首先,如果您修复了筛选代码,那么它的运行速度应该是现在的25倍,大致相当于在32个核心上成功分发当前代码所带来的预期收益。

看一下prime number summing still slow after using sieve,我展示了如何在所有语言的 C#中以1.25秒的速度筛选数字高达2,000,000,000。本文分别讨论(和基准测试)每个步骤/技术,以便您选择自己喜欢的内容并推出符合您需求的完美爆炸/降压比的解决方案。在C / C ++中,事情会更快,因为在那里你可以指望编译器为你提供小东西(至少对于像gcc或VC ++这样出色的编译器)。

第二:筛选大范围时,最重要的资源是CPU的第1级缓存。其他一切都是第二小提琴。您也可以从我的文章中的基准测试中看到这一点。要在多个CPU之间分配筛选任务,请计算系统中的L1缓存,并为每个缓存分配筛选作业(k为L1缓存数量的范围的1 / k)。这有点简化,因为您通常会为工作项的大小选择更精细的粒度,但它提供了一般的想法。

我说'缓存',而不是'核心','虚拟核心'或'线程',因为这正是您需要做的:分配作业,使每个作业都有自己的L1缓存。它的工作原理不仅取决于操作系统,还取决于系统中的特定CPU。如果两个'whatevers'共享一个L1缓存,只给两个中的一个提供一个工作而忽略另一个(或者更确切地说,设置工作的亲和力,使其可以在两个中的任何一个上运行,但不管其他地方)。 / p>

这对于操作系统API(例如Win32)来说很容易,但我对pthreads知之甚少,无法判断它是否提供了所需的精度。作为第一个近似值,您可以将线程数与可疑数量的L1缓存进行匹配。