簡體   English   中英

為什么這個程序的多線程版本更慢?

[英]Why is the multithreaded version of this program slower?

我正在嘗試學習pthreads,我一直在試驗一個試圖檢測數組上的變化的程序。 函數array_modifier()選擇一個隨機元素並切換它的值(1到0,反之亦然),然后休眠一段時間(足夠大,所以不會出現競爭條件,我知道這是不好的做法)。 change_detector()掃描數組,當元素與其先前值不匹配且等於1時,檢測到更改並使用檢測延遲更新diff數組。

當有一個change_detector()線程( NTHREADS==1 )時,它必須掃描整個數組。 當有更多線程時,每個線程都分配了一部分數組。 每個探測器線程只捕獲其數組部分的修改,因此您需要將所有4個線程的捕獲時間相加,以獲得捕獲所有更改的總時間。

這是代碼:

#include <pthread.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/time.h>
#include <time.h>

#define TIME_INTERVAL 100
#define CHANGES 5000

#define UNUSED(x) ((void) x)

typedef struct {
    unsigned int tid;
} parm;

static volatile unsigned int* my_array;
static unsigned int* old_value;
static struct timeval* time_array;
static unsigned int N;

static unsigned long int diff[NTHREADS] = {0};

void* array_modifier(void* args);
void* change_detector(void* arg);

int main(int argc, char** argv) {
    if (argc < 2) {
        exit(1);
    }

    N = (unsigned int)strtoul(argv[1], NULL, 0);

    my_array = calloc(N, sizeof(int));
    time_array = malloc(N * sizeof(struct timeval));
    old_value = calloc(N, sizeof(int));

    parm* p = malloc(NTHREADS * sizeof(parm));
    pthread_t generator_thread;
    pthread_t* detector_thread = malloc(NTHREADS * sizeof(pthread_t));

    for (unsigned int i = 0; i < NTHREADS; i++) {
        p[i].tid = i;
        pthread_create(&detector_thread[i], NULL, change_detector, (void*) &p[i]);
    }

    pthread_create(&generator_thread, NULL, array_modifier, NULL);

    pthread_join(generator_thread, NULL);

    usleep(500);

    for (unsigned int i = 0; i < NTHREADS; i++) {
        pthread_cancel(detector_thread[i]);
    }

    for (unsigned int i = 0; i < NTHREADS; i++) fprintf(stderr, "%lu ", diff[i]);
    fprintf(stderr, "\n");
    _exit(0);
}


void* array_modifier(void* arg) {
    UNUSED(arg);
    srand(time(NULL));

    unsigned int changing_signals = CHANGES;

    while (changing_signals--) {
        usleep(TIME_INTERVAL);
        const unsigned int r = rand() % N;

        gettimeofday(&time_array[r], NULL);
        my_array[r] ^= 1;
    }

    pthread_exit(NULL);
}

void* change_detector(void* arg) {
    const parm* p = (parm*) arg;
    const unsigned int tid = p->tid;
    const unsigned int start = tid * (N / NTHREADS) +
                               (tid < N % NTHREADS ? tid : N % NTHREADS);
    const unsigned int end = start + (N / NTHREADS) +
                             (tid < N % NTHREADS);
    unsigned int r = start;

    while (1) {
        unsigned int tmp;
        while ((tmp = my_array[r]) == old_value[r]) {
            r = (r < end - 1) ? r + 1 : start;
        }

        old_value[r] = tmp;
        if (tmp) {
            struct timeval tv;
            gettimeofday(&tv, NULL);
            // detection time in usec
            diff[tid] += (tv.tv_sec - time_array[r].tv_sec) * 1000000 + (tv.tv_usec - time_array[r].tv_usec);
        }
    }
}

當我編譯並運行時:

gcc -Wall -Wextra -O3 -DNTHREADS=1 file.c -pthread && ./a.out 100

我明白了:

665

但是當我編譯並運行時:

gcc -Wall -Wextra -O3 -DNTHREADS=4 file.c -pthread && ./a.out 100

我明白了:

152 190 164 242

(總計達748)。

因此,多線程程序的延遲更大。

我的cpu有6個核心。

多線程程序很少與線程數完全擴展。 在您的情況下,您使用4個螺紋測量了加速因子約為0.9(665/748)。 那不太好。

以下是需要考慮的一些因素:

啟動線程和分割工作的開銷。 對於小型作業,啟動額外線程的成本可能比實際工作大得多。 不適用於這種情況,因為開銷不包括在時間測量中。

“隨機”變化 您的線程在152和242之間變化。您應該多次運行測試並使用均值或中值。

測試的大小。 通常,您可以在更大的測試中獲得更可靠的測量(更多數據)。 但是,您需要考慮如何讓更多數據影響L1 / L2 / L3緩存中的緩存。 如果數據太大而無法容納到RAM中,則需要考慮磁盤I / O. 通常,多線程實現速度較慢,因為它們希望一次處理更多數據,但在極少數情況下它們可能更快,這種現象稱為超線性加速

線程間通信引起的開銷。 也許不是你的情況因素,因為你沒有那么多。

資源鎖定導致的開銷。 通常對cpu利用率的影響很小,但可能對實際使用的總時間產生很大影響。

硬件優化。 某些CPU 根據您使用的核心數來更改時鍾頻率

測量本身的成本 在您的情況下,將在for循環的25(100/4)次迭代中檢測到更改。 每次迭代只需幾個時鍾周期。 然后你調用gettimeofday ,這可能花費數千個時鍾周期。 所以你實際測量的是或多或少是調用gettimeofday的成本。

我會增加要檢查的值的數量和檢查每個值的成本。 我還會考慮關閉編譯器優化,因為這會導致程序執行意外操作(或完全跳過某些操作)。

簡短回答你在線程之間共享內存和線程之間的共享內存很慢。

長答案您的程序使用多個線程寫入my_array ,另一個線程從my_array讀取。 有效地, my_array由許多線程共享。

現在假設您在多核計算機上進行基准測試,您可能希望操作系統為每個線程分配不同的核心。

請記住,在現代處理器上寫入RAM非常昂貴(數百個CPU周期)。 為了提高性能,CPU具有多級緩存。 最快的緩存是小型L1緩存。 核心可以以2-3個周期的順序寫入其L1高速緩存。 L2高速緩存可能需要20-30個周期。

現在在許多CPU架構中,每個核心都有自己的L1緩存,但L2緩存是共享的。 這意味着線程(核心)之間共享的任何數據都必須通過L2緩存,這比L1緩存慢得多。 這意味着共享內存訪問往往非常慢。

最重要的是,如果您希望多線程程序運行良好,則需要確保線程不共享內存。 共享內存很慢。

除了在線程之間共享內存時,不要依賴volatile來做正確的事情,要么使用庫原子操作要么使用互斥鎖。 這是因為如果你不知道自己在做什么,有些CPU允許亂序讀寫,這可能會造成奇怪的事情。

暫無
暫無

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

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