为啥多线程更慢?

Posted

技术标签:

【中文标题】为啥多线程更慢?【英文标题】:Why is multithreaded slower?为什么多线程更慢? 【发布时间】:2013-06-06 14:20:56 【问题描述】:

所以我正在尝试编写一个查找素数的程序。该项目的真正目的只是学习多线程。首先我编写了一个单线程程序,它在 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 倍吗?

【问题讨论】:

可能会因为线程上下文切换时间而变慢 另外,iter2iter4 几乎无事可做。 你有多少个内核/CPU? @Magn3s1um:但是这里没有内存访问... 还将所有不同线程的结果输出(cout)到处理显示/控制台的单个线程的开销。这(可能)需要某种同步。 【参考方案1】:

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

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

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

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

Single thread: 15.04s. 
Four threads: 11.25s

不打印:

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

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

【讨论】:

注释掉cout,编译器会将整个函数优化掉。它肯定会跑得更快:-D @VladLazarenko 我非常怀疑该函数本身会消失,如果确实如此,请将您的优化退后一步。 因此,对所有素数求和并在函数末尾打印结果。 我很确定每个线程只是在标准输出上抛出素数,而不关心顺序或任何事情,所以它不应该放慢很多。 @DmobbJr.:我刚刚从我的代码中添加了一些计时,它使用和不使用cout(它是相同的源代码,它有一个if (verbose) 来决定它是否是否应该打印。【参考方案2】:

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

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

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 的筛子),但它简单、直接,而且速度快大约两到三倍(以上时间基于使用此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 仍在寻找素数(同样,当线程 3 和 4 仍在计算时开始从线程 2 写入数据,并在线程 4 仍在计算时从线程 3 写入数据。

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

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

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

【讨论】:

【参考方案3】:

这取决于操作系统给你的代码运行多少 CPU。这些线程中的每一个都受 CPU 限制,因此如果您只有一个 CPU,它将运行一个线程一段时间、对其进行时间切片、运行下一个线程等,这不会更快,而且可能会更慢,具体取决于线程交换的开销。至少在 solaris 上,告诉它您希望所有线程同时运行是值得的。

我没有遇到过像另一张海报所建议的那样对输出进行序列化的实现。通常你会得到类似的输出

235 iisi s  ppprririimmme
ee

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

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

【讨论】:

【参考方案4】:

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

【讨论】:

超线程只有在您通过读取和写入文件(或sleep 调用)而创建大量锁时才真正有用,这会浪费处理器周期。因此,当您有一个永远不会像这样退出的优化循环时,超线程只会浪费循环设置上下文切换。 您的意思是由于读/写内存而阻塞。如果整个线程在操作系统中进入睡眠状态,超线程对非超线程没有帮助。但是,如果处理器必须等待几个时钟周期才能从内存中获取某些内容(或类似操作),这确实会有所帮助。【参考方案5】:

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

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

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

【讨论】:

或者您可以在每个线程中缓存结果,并在您填充一些缓冲区或达到计算结束后将它们批量写入。使用线程队列仍然需要一个同步点来推送和弹出。 这并不能真正解决问题,因为你必须确保你的putc_unlocked 不会相互干扰,使用某种互斥锁等。[我想我可以想出一个不那么广泛的方法来从一个整数创建一个字符串] @MatsPetersson 我不认为我们的目标是在这里创建一个字符串,只是为了输出。这是我所知道的打印整数的最节省内存和速度的方法,但是,如果我错了,请告诉我。 @Dan 你可以构建线程安全的低锁队列,如果它们只保存一个整数,那应该不会太难。 你不能在没有任何锁定的情况下直接调用putc_unlocked。这将破坏流。文档说,“调用者负责在调用之前用flockfile(3) 锁定流。”

以上是关于为啥多线程更慢?的主要内容,如果未能解决你的问题,请参考以下文章

啥?用了并行流还更慢了。。

oracle 可以多线程插入吗

多线程文件复制比多核CPU上的单个线程慢得多

串口传输用不用使用多线程 为啥

为啥并行多线程代码执行比顺序慢?

为啥我的工作线程没有使用 python 多线程产生