为什么此程序的多线程版本较慢?

时间:2015-08-14 08:37:54

标签: c performance pthreads

我正在尝试学习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);
        }
    }
}

当我编译&amp;像这样跑:

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

我明白了:

665

但是当我编译&amp;像这样跑:

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

我明白了:

152 190 164 242

(总计达748)。

因此,多线程程序的延迟更大。

我的cpu有6个核心。

2 个答案:

答案 0 :(得分:3)

多线程程序很少与线程数完全扩展。在您的情况下,您使用4个螺纹测量了加速因子约为0.9(665/748)。那不太好。

以下是需要考虑的因素:

启动线程和分割工作的开销。对于小型作业,启动额外线程的成本可能比实际工作大得多。不适用于这种情况,因为开销不包括在时间测量中。

"Random" variations。你的线程在152到242之间变化。你应该多次运行测试并使用平均值或中值。

测试的大小。通常,您可以在更大的测试中获得更可靠的测量(更多数据)。但是,您需要考虑如何让更多数据影响L1 / L2 / L3缓存中的缓存。如果数据太大而无法容纳到RAM中,则需要考虑磁盘I / O.通常,多线程实现较慢,因为它们希望一次处理更多数据,但在极少数情况下它们可能更快,这种现象称为super-linear speedup

线程间通信导致的开销。也许不是你的情况中的一个因素,因为你没有那么多。

资源锁定导致的开销。通常对cpu利用率的影响很小,但可能会对实际使用的总时间产生很大影响。

硬件优化。某些CPU change the clock frequency取决于您使用的核心数。

测量本身的成本。在您的情况下,将在for循环的25(100/4)次迭代中检测到更改。每次迭代只需几个时钟周期。然后你调用gettimeofday,这可能花费数千个时钟周期。因此,您实际测量的内容或多或少是调用gettimeofday的成本。

我会增加要检查的值的数量以及检查每个值的成本。我还会考虑关闭编译器优化,因为这会导致程序执行意外操作(或完全跳过某些操作)。

答案 1 :(得分:3)

简答 你在线程之间共享内存和线程之间的共享内存很慢。

长答案 您的程序使用多个线程写入my_array,另一个线程从my_array读取。有效地my_array由许多线程共享。

现在让我们假设您在多核计算机上进行基准测试,您可能希望操作系统为每个线程分配不同的核心。

请记住,在现代处理器上写入RAM非常昂贵(数百个CPU周期)。为了提高性能,CPU具有多级缓存。最快的缓存是小型L1缓存。核心可以以2-3个周期的顺序写入其L1高速缓存。 L2缓存可能需要20到30个周期。

现在在许多CPU架构中,每个核心都有自己的L1缓存,但L2缓存是共享的。这意味着线程(核心)之间共享的任何数据都必须通过L2缓存,这比L1缓存慢得多。这意味着共享内存访问速度往往很慢。

最重要的是,如果您希望多线程程序运行良好,则需要确保线程不共享内存。共享内存很慢。

<强>除了 在线程之间共享内存时,不要依赖volatile来做正确的事情,要么使用库原子操作要么使用互斥锁。这是因为如果你不知道自己在做什么,有些CPU允许乱序读写可能会做些奇怪的事情。