14

所以我正在尝试编写一个找到素数的程序。该项目的真正目的只是学习多线程。首先,我编写了一个单线程程序,它在 1 分钟内找到了 13,633,943 个。我的多线程版本只有 10,025,627。

这是我的单线程程序代码

#include <iostream>

using namespace std;

bool isprime(long num)
{
    long lim = num/2;
    if(num == 1)
    {
        return 0;
    }
    for(long i = 2; i <= lim; i++)
    {
        if (num % i == 0)
        {
            return 0;
        }
        else{ lim = num/i; }
    }
    return 1;
}

int main()
{
    long lim;
    cout << "How many numbers should I test: ";
    cin >> lim;
    for(long i = 1; i <= lim || lim == 0; i++)
    {
        if(isprime(i))
        {
            cout << i << endl;
        }
    }
}

这是我的多线程程序的代码。

extern"C"
{
    #include <pthread.h>
    #include <unistd.h>
}
#include <iostream>

using namespace std;

bool isprime(long num);
void * iter1(void * arg);
void * iter2(void * arg);
void * iter3(void * arg);
void * iter4(void * arg);


int main()
{
    //long lim;
    //cout << "How many numbers should I test: ";
    //cin >> lim;
    pthread_t t1;
    char mem1[4096];//To avoid false sharing. Needed anywhere else?
    pthread_t t2;
    char mem2[4096];//These helped but did not solve problem.
    pthread_t t3;
    pthread_create(&t1, NULL, iter1, NULL);
    pthread_create(&t2, NULL, iter2, NULL);
    pthread_create(&t3, NULL, iter3, NULL);
    iter4(0);
}

bool isprime(long num)
{
    long lim = num/2;
    if(num == 1)
    {
        return 0;
    }
    for(long i = 2; i <= lim; i++)
    {
        if (num % i == 0)
        {
            return 0;
        }
        else{ lim = num/i; }
    }
    return 1;
}

void * iter1(void * arg)
{
    for(long i = 1;; i = i + 4)
    {
        if(isprime(i))
        {
            cout << i << endl;
        }
    }
return 0;
}

void * iter2(void * arg)
{
    for(long i = 2;; i = i + 4)
    {
        if(isprime(i))
        {
            cout << i << endl;
        }
    }
return 0;
}

void * iter3(void * arg)
{
    for(long i = 3;; i = i + 4)
    {
        if(isprime(i))
        {
            cout << i << endl;
        }
    }
return 0;
}

void * iter4(void * arg)
{
    for(long i = 4;; i = i + 4)
    {
        if(isprime(i))
        {
            cout << i << endl;
        }
    }
return 0;
}

让我特别困惑的是系统监视器报告单线程的 CPU 使用率为 25%,多线程的使用率为 100%。这不应该意味着它的计算量是原来的 4 倍吗?

4

5 回答 5

12

我相当肯定cout它是一个共享资源——即使它实际上以正确的顺序正确地打印了每个数字,这样做也会大大减慢速度。

我做过类似的事情(它更灵活,并使用原子操作来“选择下一个数字”),并且在我的四核机器上几乎快 4 倍。但这只是在我不打印任何东西的情况下。如果它打印到控制台,它会慢很多 - 因为很多时间都用于洗牌像素而不是实际计算。

注释掉该cout << i << endl;行,它会运行得更快。

编辑:使用我的测试程序,打印:

Single thread: 15.04s. 
Four threads: 11.25s

不打印:

Single threads: 12.63s.
Four threads: 3.69s.

3.69 * 4 = 14.76s,但time我的 Linux 机器上的命令显示总运行时间为 12.792s,因此显然有一点时间所有线程都没有运行 - 或者一些记账错误......

于 2013-06-06T14:27:31.650 回答
7

我认为您当前的很多问题是您正在参与可以真正操作多线程的部分(查找素数)并将其隐藏在噪音中(将输出写入控制台的时间)。

为了了解这有多大的影响,我稍微改写了您的主要内容,以将打印素数与查找素数分开。为了使计时更容易,我还让它从命令行而不是交互地获取限制,给出:

int main(int argc, char **argv) {
    if (argc != 2) {
        std::cerr << "Usage: bad_prime <limit:long>\n";
        return 1;
    }
    std::vector<unsigned long> primes;

    unsigned long lim = atol(argv[1]);

    clock_t start = clock();

    for(unsigned long i = 1; i <= lim; i++)
        if(isprime(i))
            primes.push_back(i);
    clock_t stop = clock();

    for (auto a : primes)
        std::cout << a << "\t";

    std::err << "\nTime to find primes: " << double(stop-start)/CLOCKS_PER_SEC << "\n";
}

跳过数千行素数本身,我得到如下结果:

Time to find primes: 0.588


Real    48.206
User    1.68481
Sys     3.40082

所以——大约需要半秒才能找到质数,打印它们需要超过 47 秒。假设真正的目的是将输出写入控制台,我们不妨停在那里。即使多线程可以完全消除寻找素数的时间,我们仍然只能将最终时间从 ~48.2 秒更改为 ~47.6 秒——不太值得。

因此,目前我假设真正的意图是将输出写入文件之类的东西。由于进行多线程代码的工作似乎毫无意义,但在每个线程中运行效率极低的代码,我想我会优化(或者,至少,去悲观化)单线程代码作为开始观点。

首先,我删除了endl并将其替换为"\n". 通过将输出定向到文件,这将运行时间从 0.968 秒减少到 0.678 秒——endl除了写入换行符之外还刷新缓冲区,并且缓冲区刷新大约占整个程序所用时间的三分之一。

在同样的基础上,我冒昧地将你重写isprime为至少效率低一些的东西:

bool isprime(unsigned long num) {
    if (num == 2)
        return true;

    if(num == 1 || num % 2 == 0)
        return false;

    unsigned long lim = sqrt(num);

    for(unsigned long i = 3; i <= lim; i+=2)
        if (num % i == 0)
            return false;

    return true;
}

这当然可以进行更多改进(例如,Eratosthenes 的筛子),但它简单、直接,并且速度大约是其两到三倍(以上时间基于使用 this isprime,而不是您的)。

在这一点上,对主要发现进行多线程处理至少有一定的意义:主要发现在 0.6 秒中大约需要 0.5 秒,即使我们只能将速度提高一倍,我们也应该看到总时间的显着差异.

将输出与主要发现分开也为我们编写多线程版本的代码提供了更好的基础。随着每个线程将其结果写入一个单独的向量,我们可以获得有意义的(不是交错的)输出,而无需进行锁定cout等等——我们分别计算每个块,然后按顺序打印出每个向量。

代码可能如下所示:

#include <iostream>
#include <vector>
#include <time.h>
#include <math.h>
#include <thread>

using namespace std;

bool isprime(unsigned long num) {
    // same as above
}

typedef unsigned long UL;

struct params { 
    unsigned long lower_lim;
    unsigned long upper_lim;
    std::vector<unsigned long> results;

    params(UL l, UL u) : lower_lim(l), upper_lim(u) {}
};

long thread_func(params *p) { 
    for (unsigned long i=p->lower_lim; i<p->upper_lim; i++)
        if (isprime(i))
            p->results.push_back(i);
    return 0;
}

int main(int argc, char **argv) {
    if (argc != 2) {
        std::cerr << "Usage: bad_prime <limit:long>\n";
        return 1;
    }

    unsigned long lim = atol(argv[1]);

    params p[] = {
        params(1, lim/4),
        params(lim/4, lim/2),
        params(lim/2, 3*lim/4),
        params(3*lim/4, lim)
    };

    std::thread threads[] = {
        std::thread(thread_func, p), 
        std::thread(thread_func, p+1),
        std::thread(thread_func, p+2),
        std::thread(thread_func, p+3)
    };

    for (int i=0; i<4; i++) {
        threads[i].join();
        for (UL p : p[i].results)
            std::cout << p << "\n";
    }
}

在与以前相同的机器上运行它(一个相当旧的双核处理器),我得到:

Real    0.35
User    0.639604
Sys     0

这似乎扩展得非常好。如果我们从中获得的只是多核计算,我们希望看到找到素数除以 2 的时间(我在双核处理器上运行它)并且将数据写入磁盘的时间保持不变(多线程不会加速我的硬盘驱动器)。基于此,完美缩放应该给我们 0.59/2 + 0.1 = 0.40 秒。

我们看到的(不可否认的)微小改进很可能源于这样一个事实,即我们可以开始将数据从线程 1 写入磁盘,而线程 2、3 和 4 仍在寻找素数(同样,开始写入来自线程 2 的数据,而线程 3 和 4 仍在计算,并在线程 4 仍在计算时从线程 3 写入数据)。

我想我应该补充一点,我们看到的改进足够小,它也可能是时间上的简单噪音。然而,我确实多次运行单线程和多线程版本,虽然两者都有一些变化,但多线程版本始终比计算速度的提高要快。

我几乎忘记了:为了了解这对整体速度有多大影响,我进行了一次测试,看看需要多长时间才能找到高达 13,633,943 的素数,而原始版本在一分钟内就找到了。尽管我几乎可以肯定使用的是较慢的 CPU(大约 7 岁的 Athlon 64 X2 5200+),但这个版本的代码在 12.7 秒内完成了。

最后一点:至少目前,我省略了您插入的填充以防止错误共享。根据我得到的时间,它们似乎没有必要(或有用)。

于 2013-06-06T17:19:43.767 回答
1

我相信 Oli Charlesworth 在超线程问题上一针见血。我认为超线程实际上就像有两个内核。它不是。我将其更改为仅使用两个线程,我得到了 22,227,421,这几乎是两倍的速度。

于 2013-06-06T14:49:56.890 回答
1

This depends rather on how many CPUs your code gets given to run on by the OS. Each of these threads is CPU bound so if you have just the one CPU it's going to run one thread for a bit, timeslice it, run the next thread, etc, which won't be any faster and may well be slower, depending on the overhead of a thread swap. And on solaris, at least, it's worth while telling it you want all the threads to run at once.

I've not come across an implementation where output is serialised like is suggested by the other poster. Normally you get output like

235 iisi s  ppprririimmme
ee

so your output may well indicate the O/S is not allocating you multiple threads.

Another issue you might be hitting is that output to a console is incredibly slow compared to output to a file. It may be worth sending the output from your program to a file and seeing how fast it goes like that.

于 2013-06-06T14:48:14.830 回答
-2

虽然@MatsPetersson 是正确的(至少对于基于 POSIX 的系统来说,stdout它是一种共享资源),但他没有提供解决该问题的方法,因此您可以通过以下方法消除那些讨厌的锁。

POSIX C 定义了一个函数,putc_unlocked,它将做与 完全相同的事情putc,但没有锁定(惊喜)。然后,使用它,我们可以定义我们自己的函数,该函数将在不锁定的情况下打印整数,并且比多线程场景更快coutprintf在多线程场景中更快:

void printint_unlocked(FILE *fptr, int i) {
    static int digits[] = {
        1,
        10,
        100,
        1000,
        10000,
        100000,
        1000000,
        10000000,
        100000000,
        1000000000,
    };

    if (i < 0) {
        putc_unlocked('-', fptr);
        i = -i;
    }

    int ndigits = (int) log10(i);
    while (ndigits >= 0) {
        int digit = (i / (digits[ndigits])) % 10;

        putc_unlocked('0' + digit, fptr);

        --ndigits;
    }
}

请注意,这种方法完全有可能出现竞争条件,从而导致输出中的数字发生冲突。如果您的算法最终没有发生任何冲突,您仍然应该获得多线程代码的性能提升。

第三个也是最后一个选项(对于您的用例来说可能过于复杂)是在另一个线程上创建一个事件队列,并从该线程执行所有打印,从而导致没有竞争条件,并且线程之间没有锁定问题。

于 2013-06-06T14:50:03.120 回答