簡體   English   中英

為什么多線程會變慢?

[英]Why is multithreaded slower?

所以我正在嘗試編寫一個找到素數的程序。 該項目的真正目的只是學習多線程。 首先,我編寫了一個單線程程序,它在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%,多線程的CPU使用率為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,但我的Linux機器上的time命令顯示總運行時間為12.792秒,因此顯然有一點時間所有線程都沒有運行 - 或者一些會計錯誤......

我認為你目前的很多問題是你正在采用真正可以運行多線程(找到素數)並將其隱藏在噪聲中的部分(將輸出寫入控制台的時間)。

為了了解它有多大的影響,我重新編寫了你的​​主要內容,分別打印素數與尋找素數。 為了使計時更容易,我還從命令行而不是交互式獲取限制,給出:

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.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秒內完成。

最后一點說明:至少在目前,我已經省去了你要插入的填充以防止錯誤共享。 根據我所獲得的時間,它們似乎沒有必要(或有用)。

這取決於您的代碼在操作系統上運行的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;
    }
}

請注意,使用此方法可能存在競爭條件,導致數字在輸出中發生碰撞。 如果您的算法沒有遇到任何沖突,您仍應該獲得多線程代碼的性能提升。

第三個也是最后一個選項(可能對你的用例來說太復雜了)是在另一個線程上創建一個事件隊列,並從該線程進行所有打印,導致沒有競爭條件,並且線程之間沒有鎖定問題。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM