为什么多线程较慢

Why is multithreaded slower?

本文关键字:多线程 为什么      更新时间:2023-10-16

所以我想写一个程序来找到素数。这个项目的真正目的只是学习多线程。首先,我写了一个单线程程序,它在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倍的计算吗?

我相当确定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的总运行时间,所以显然有一点时间当所有线程都没有运行-或者一些会计错误…

我认为你目前的很多问题是,你正在采取的部分,可以真正操作多线程(找到的prime),并把它埋在噪音(时间写输出到控制台)。

为了了解这有多大的影响,我重写了你的main代码,把打印质数和查找质数分开。为了使计时更容易,我还让它从命令行而不是交互式地获取限制,如下所示:

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;
}

这当然是开放的更多的改进(例如,埃拉托色尼筛子),但它很简单,直接,大约两到三倍的速度(以上时间是基于使用这个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秒。

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

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

我几乎忘记了:为了了解这对整体速度有多大影响,我运行了一个测试,看看找到13,633,943以下的质数需要多长时间,你的原始版本在一分钟内找到了。即使我几乎肯定使用较慢的CPU(约7年的Athlon 64 X2 5200+),这个版本的代码在12.7秒内完成。

最后一点:至少现在,我已经去掉了你为防止虚假分享而插入的填充。根据我得到的时间,它们似乎没有必要(或有用)。

这取决于你的代码在多少个cpu上运行。这些线程中的每一个都是CPU绑定的,所以如果你只有一个CPU,它将运行一个线程一点,时间片,运行下一个线程,等等,这不会更快,可能会更慢,这取决于线程交换的开销。至少在solaris上,告诉它您希望所有线程同时运行是值得的。

我还没有遇到像其他海报建议的那样序列化输出的实现。通常你会得到像

这样的输出
235 iisi s  ppprririimmme
ee

所以你的输出可能表明O/S没有为你分配多个线程。

您可能遇到的另一个问题是,与输出到文件相比,输出到控制台的速度非常慢。将程序的输出发送到一个文件中,看看它的运行速度有多快,这可能是值得的。

我相信Oli Charlesworth用超线程问题击中了它的头。我认为超线程实际上就像有两个内核。它不是。我将其更改为仅使用两个线程,并且我得到了22,227,421,这几乎是快的两倍。

虽然@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;
    }
}

注意,这个方法完全有可能存在竞争条件,导致输出中的数字冲突。如果你的算法最终没有任何冲突,你仍然应该得到多线程代码的性能提升。

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