为什么多线程不能提高该程序查找素数的性能? 计时

问题描述

运行时间大致相同,而与线程数无关。我很难弄清楚为什么。我知道线程可以按预期的方式并行运行,但是我什至没有一个很好的猜测,为什么没有任何性能改进。 (对于单线程和多线程,查找所有小于800万的素数大约需要21秒)。这是怎么回事?

typedef struct prime_finder_vars {
    long from;
    long to;
    int idx;
} PrimeFinderVars;

int is_prime(long num) {
    int limit = round(sqrt(num));
    for (long i = 2; i <= limit; i++) {
        if (num % i == 0)
            return FALSE;
    }
    return TRUE;
}

void *prime_finder(void *pf) {

    PrimeFinderVars *pf_vars = (PrimeFinderVars *) pf;

    long next_cand = pf_vars->from;
    while (next_cand < pf_vars->to) {
        if (is_prime(next_cand)) {
            ++counts[pf_vars->idx];
        }
        next_cand += 2;
    }
    return pf;
}


int main(void) {

    struct timespec start;
    struct timespec end;
    double start_sec,end_sec,elapsed_sec;
    int sum = 0;

    clock_gettime(CLOCK_REALTIME,&start);

    pthread_t threads[NUM_THREADS];
    PrimeFinderVars vars[NUM_THREADS];

    int slice_size = SEARCH_RANGE / NUM_THREADS;

    for (int i = 0; i < NUM_THREADS; i++) {

        vars[i].from = i * slice_size + 1;
        vars[i].to = (i + 1) * slice_size;
        vars[i].idx = i;

        pthread_create(&threads[i],NULL,prime_finder,&vars[i]);

    }

    for (int i = 0; i < NUM_THREADS; i++) {
        pthread_join(threads[i],NULL);
        sum += counts[i];
    }

    clock_gettime(CLOCK_REALTIME,&end);

    start_sec = start.tv_sec + start.tv_nsec / NANO_PER_SEC;
    end_sec = end.tv_sec + end.tv_nsec / NANO_PER_SEC;
    elapsed_sec = end_sec - start_sec;
}

解决方法

线程之间的工作负载不能很好地平衡。每个线程必须检查大约相同数量的候选对象,但是与索引较低的线程相比,索引较高的线程要花更多的时间检查每个候选对象。

我会这样重写主循环:

  $("#customFile").change(function() { 
    var fileName = $("#customFile").val();  

    if(fileName) { // returns true if the string is not empty   
        $('.picture-selected').addClass('disable-inputs'); 
        $('#btn').removeClass('disabled');   
    } else { // no file was selected 
      $('.picture-selected').removeClass('disable-inputs'); 
      $('#btn').addClass('disabled');
    }  
  });

for (long candidate = pf_vars->idx; candidate < SEARCH_RANGE; candidate += NUM_THREADS) { if (is_prime (candidate)) { ++counts [pf_vars->idx]; } } 必须本身就是要使其有效工作。

此外,我怀疑您的代码会产生正确的结果,因为如果NUM_THREADS是偶数,pf_vars->from将只检查没有太大意义的偶数。

此外,线程仅在它们在不同的内核上运行时才并行运行。如果线程数远远大于内核数,则性能会下降,因为在多个线程之间切换内核也需要一些时间。

,

这是一个有趣的问题。米哈伊尔·弗拉基米罗夫(Mikhail Vladimirov)所说的一切都是真的,但我决定在笔记本电脑上做一些测试,看看能得到什么。我的笔记本电脑是具有八核i9的现代MacBook Pro。我不确定它是否是超线程的,但这是我的结果:

Time to execute by threads 我测试的线程数在1到50之间,搜索范围为10,000,000。

使用一个线程花费将近11秒钟,但是使用16个线程会迅速下降到1.5秒左右,此后并没有任何改善。

我的结论是

  1. 我对Mikhail关于线程功能成本的答案的评论是错误的,至少在我的平台上是这样。我看到更多线程不会增加开销
  2. 您的线程库出问题了。

我想您可能需要使自己满意,线程确实在单独的内核上并行运行。您的结果的一种解释可能是它们都在争夺同一个CPU。


只是为了好玩,我决定尝试对该程序进行性能分析。

Profile of the first few iterations of thread count

每一步代表另一个核心达到100%。我不确定为什么具有三个线程的prt不能达到300%,但是您可以看到具有四个线程的prt可以立即上升到400%,而下降到100%。这是因为您将任务划分为相等的范围,并且处理较小数字的线程会更快结束。


前16个数据点

Threads Time
1   11.893418
2   7.352520
3   5.117278
4   4.062026
5   3.511605
6   2.892274
7   2.401555
8   2.172573
9   1.910534
10  1.864023
11  1.860944
12  1.369277
13  1.628883
14  1.196646
15  1.626215
16  1.548878

我用来生成测试结果的代码(从您的代码稍作修改)。

#include <stdio.h>
#include <pthread.h>
#include <math.h>
#include <stdbool.h>

#define SEARCH_RANGE    10000000
#define NANO_PER_SEC    1000000000

typedef struct prime_finder_vars {
    long from;
    long to;
    int* count;
} PrimeFinderVars;

int is_prime(long num) {
    int limit = round(sqrt(num));
    for (long i = 2; i <= limit; i++) {
        if (num % i == 0)
            return false;
    }
    return true;
}


void *prime_finder(void *pf)
{

    PrimeFinderVars *pf_vars = (PrimeFinderVars *) pf;

    long next_cand = pf_vars->from;
    while (next_cand < pf_vars->to)
    {
        if (is_prime(next_cand))
        {
            (*pf_vars->count)++ ;
        }
        next_cand += 2;
    }
    return pf;
}


void trial(int numThreads)
{
    struct timespec start;
    struct timespec end;
    double start_sec,end_sec,elapsed_sec;
    int sum = 0;

    clock_gettime(CLOCK_REALTIME,&start);

    int counts[numThreads];
    pthread_t threads[numThreads];
    PrimeFinderVars vars[numThreads];

    int slice_size = SEARCH_RANGE / numThreads;

    for (int i = 0; i < numThreads; i++)
    {
        counts[i] = 0;
        vars[i].from = i * slice_size + 1;
        vars[i].to = (i + 1) * slice_size;
        vars[i].count = &counts[i];

        pthread_create(&threads[i],NULL,prime_finder,&vars[i]);

    }

    for (int i = 0; i < numThreads; i++)
    {
        pthread_join(threads[i],NULL);
        sum += counts[i];
    }

    clock_gettime(CLOCK_REALTIME,&end);

    start_sec = (double)start.tv_sec + (double)start.tv_nsec / NANO_PER_SEC;
    end_sec = (double)end.tv_sec + (double)end.tv_nsec / NANO_PER_SEC;
    elapsed_sec = end_sec - start_sec;
    printf("%d\t%f\n",numThreads,elapsed_sec);
}

int main()
{
    printf("Threads\tTime\n");
    for (int threads = 1 ; threads <= 50 ; ++threads)
    {
        trial(threads);
    }
}


在最后一两天里,我进一步追求了这一点。首先,令我着迷的是为什么似乎要出现双倍的计时:在大约12个线程之后,运行将花费1.5秒或1秒。我在上面进行理论分析是因为Mikhail提到了这个错误,因此我绘制了每个线程数的实际答案,发现虽然答案通常在664579左右,但通常是答案的一半左右,并且毫无疑问,当答案是真实答案,对应于两条时序线中的较低者。 enter image description here

所以我修复了该错误,双线效果消失了。但是,根据线程数的不同,我仍然得到多个答案。 enter image description here

这样做的原因是还有两个错误。

  1. 原始算法无法测试每个范围内的最高排名。
  2. 范围的大小是通过将搜索范围除以线程数而得到的整数。除非没有余数,否则不会检查搜索范围顶部的数字。

我修复了两个错误并进行了第三次运行。这并没有明显影响时间,但是对于使用的每个线程数,我都得到相同的答案。

为比较起见,我写了一个Eratosthenes筛子,并设定了时间。使用它和一个线程只需0.2秒,比最快数量的内核快大约七倍。

我已经发布了spreadsheet of the results,其中有一个git repo of the code

,

在将2和3识别为素数之后,所有剩余素数都采用6N±1的形式表示,其中N> =1。要平衡K个线程之间的工作负载,您应该让K个线程中的每个线程逐步执行自己的N的值序列:线程T在1 + T + K * X上工作,其中每个线程的X序列从0开始。如果您有8个线程,则意味着线程0在N₀= {1,9,17,…}等上工作。这仍然意味着线程K-1比线程0做更多的工作,因为它处理的是更大的数字,但是差异是比水平分割范围要少得多。这意味着您需要为每个线程提供一个起始编号S和线程总数K,然后该线程将把计数器x设置为值0、1 ...,并检查素数6(S + xK)±1。

现在,以此为基础来创建多线程的主查找器。这与您的代码紧密相关。它确实使用了我在GitHub上的SOQ(堆栈溢出问题)存储库中提供的一些代码,它们分别是文件timer.ctimer.hstderr.cstderr.h中的文件src/libsoq子目录。它使用从我的SOQ存储库的src/Primes子目录的文件isprime()中找到的IsPrime3B()重命名的函数isprime.c。该程序是prime-thread.c来自相同的src/Primes目录。

/* SO 6438-1942 */
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include "stderr.h"
#include "timer.h"

#define NANO_PER_SEC 1.0E9
enum { NUM_THREADS = 8 };
enum { MAX_NUMBER  = 10000000 };

static size_t counts[NUM_THREADS];

static const unsigned int small_primes[] =
{
     5,7,11,13,17,19,23,29,31,37,41,43,47,53,59,61,67,71,73,79,83,87,89,91,97
};
enum { NUM_SMALL_PRIMES = sizeof(small_primes) / sizeof(small_primes[0]) };

/* IsPrime3B() from isprime.c - renamed is_prime() */
static int is_prime(unsigned number)
{
    if (number <= 1)
        return 0;
    if (number == 2 || number == 3)
        return 1;
    if (number % 2 == 0 || number % 3 == 0)
        return 0;
    for (unsigned i = 0; i < NUM_SMALL_PRIMES; i++)
    {
        if (number == small_primes[i])
            return 1;
        if (number % small_primes[i] == 0)
            return 0;
    }
    /* After 97,the next prime numbers are 101,103,107,109 */
    /*
    ** It would be feasible to start this loop from:
    ** i = (((small_primes[NUM_SMALL_PRIMES - 1] + 1) / 6) + 1) * 6
    */
    for (unsigned i = 102; (i - 1) <= number / (i - 1); i += 6)
    {
        if (number % (i - 1) == 0 || number % (i + 1) == 0)
            return 0;
    }
    return 1;
}

typedef struct prime_finder_vars
{
    unsigned from;
    unsigned to;
    unsigned increment;   /* Number of threads */
    unsigned idx;
} PrimeFinderVars;

static void *prime_finder(void *pf)
{
    PrimeFinderVars *pf_vars = (PrimeFinderVars *) pf;
    printf("Thread %u: from = %u,to = %u,inc = %u\n",pf_vars->idx,pf_vars->from,pf_vars->to,pf_vars->increment);

    unsigned next = pf_vars->from;
    while (next < pf_vars->to) {
        unsigned six_n = 6 * next;
        if (is_prime(six_n - 1))
            ++counts[pf_vars->idx];
        if (is_prime(six_n + 1))
            ++counts[pf_vars->idx];
        next += pf_vars->increment;
    }
    printf("Thread %u: done\n",pf_vars->idx);
    return pf;
}

int main(int argc,char **argv)
{
    err_setarg0(argv[0]);
    if (argc != 1)
        err_usage("");
    struct timespec start;
    struct timespec end;
    double start_sec,elapsed_sec;
    int sum = 0;
    Clock clk;

    clk_init(&clk);

    clk_start(&clk);
    clock_gettime(CLOCK_REALTIME,&start);

    pthread_t threads[NUM_THREADS];
    PrimeFinderVars vars[NUM_THREADS];

    int max_n = (MAX_NUMBER + 5) / 6;

    for (int i = 0; i < NUM_THREADS; i++)
    {
        vars[i].from = i + 1;
        vars[i].to = max_n;
        vars[i].idx = i;
        vars[i].increment = NUM_THREADS;

        int rc;
        if ((rc = pthread_create(&threads[i],&vars[i])) != 0)
            err_syserr("failed to create thread %d: ",i);
    }

    for (int i = 0; i < NUM_THREADS; i++)
    {
        pthread_join(threads[i],&end);
    clk_stop(&clk);

    start_sec = start.tv_sec + start.tv_nsec / NANO_PER_SEC;
    end_sec = end.tv_sec + end.tv_nsec / NANO_PER_SEC;
    elapsed_sec = end_sec - start_sec;
    printf("Time 1: %.6f\n",elapsed_sec);
    char buffer[32];
    printf("Time 2: %s\n",clk_elapsed_us(&clk,buffer,sizeof(buffer)));

    /* Because 2 and 3 are primes but are not analyzed */
    size_t t_count = 2;
    for (int i = 0; i < NUM_THREADS; i++)
    {
        t_count += counts[i];
        printf("%d: %7zu primes found\n",i,counts[i]);
    }
    printf("Total primes found up to %d = %zu\n",MAX_NUMBER,t_count);

    return 0;
}

示例输出:

$ timecmd -u -- prime-thread
2020-10-16 12:15:05.101785 [PID 75174] prime-thread
Thread 0: from = 1,to = 1666667,inc = 8
Thread 7: from = 8,inc = 8
Thread 2: from = 3,inc = 8
Thread 3: from = 4,inc = 8
Thread 5: from = 6,inc = 8
Thread 4: from = 5,inc = 8
Thread 6: from = 7,inc = 8
Thread 1: from = 2,inc = 8
Thread 0: done
Thread 6: done
Thread 4: done
Thread 7: done
Thread 3: done
Thread 5: done
Thread 2: done
Thread 1: done
Time 1: 0.231135
Time 2: 0.231135
0:   83090 primes found
1:   83176 primes found
2:   83023 primes found
3:   82996 primes found
4:   83060 primes found
5:   82995 primes found
6:   83179 primes found
7:   83058 primes found
Total primes found up to 10000000 = 664579
2020-10-16 12:15:05.341489 [PID 75174; status 0x0000]  -  0.239704s
$

少于1000万的确有664579个素数。

请注意,timecmd程序计算prime-thread的整个运行时间(启动和打印),而内部计时仅计算线程的创建,运行和终止时间。这占了8 ms的时序差异。 (这是我用于定时命令的自制程序。它与系统提供的time命令大致相似,但也有很大不同。)

给出最多10,000个素数的列表,计算每个线程应该找到多少个素数是可行的。既然总数是正确的,那么那里就不太可能出现问题。

计时

请注意,该问题表明,最多计算8,000个素数需要21秒。这段代码花了0.231秒才能计算出最多10,000个素数的数量。 这表明使用的isprime()函数不如我使用的函数好。

实际上,显示的代码是:

int is_prime(long num) {
    int limit = round(sqrt(num));
    for (long i = 2; i <= limit; i++) {
        if (num % i == 0)
            return FALSE;
    }
    return TRUE;
}

这比必要的工作要多得多。只有一个偶数质数2。此代码检查4、6、8…都是非质数。这是所需工作量的两倍。检查2,然后仅检查奇数将是一个重大改进。检查2、3,然后匹配6N±1的数字会带来另一个改进。

即使如此,检查三分之一的数据也只会使性能提高3倍。不平衡的工作负载可能是一个更大的因素。对于8个线程(0..7),线程7的工作范围为7,000..8,000,并且与线程0的工作范围为0..1,000相比,它要执行的计算要多得多,即使它要处理的素数更少计数。

问题未显示完整的MCVE(Minimal,Complete,Verifiable Example —或MRE或SO现在使用的任何名称) 或 SSCCE(Short,Self-Contained,Correct Example)。它没有显示正在使用多少个线程。

我尚未(但也许应该)对prime-thread.c进行参数化,以获取可变数量的线程和可变范围的分析对象(并删除调试打印的线程),并查看其变化行为。在我的机器上,更多线程不太可能改善性能。可能线程越少越好。

我有一个程序primes,该程序使用Eratosthenes筛子在给定范围内打印质数。当输出进入(SSD)文件或/dev/null时,它会在大约0.135秒内打印全部664,579张预装纸,达到1000万张。这比prime-thread算素数要快得多。更好的算法有很多好处。这个isprime()函数为每个候选编号进行了大量计算。

从中可以得出两个教训:

  • 算法很重要。
  • 线程不是加快速度的灵丹妙药。