线程实现会降低性能

时间:2013-05-26 14:40:36

标签: linux windows performance pthreads

我在C中实施了一个小程序,使用蒙特卡罗方法计算PI(主要是因为个人兴趣和培训)。在实现了基本代码结构之后,我添加了一个命令行选项,允许执行线程化的计算。

我预计会有大幅加速,但我很失望。命令行概要应该是清楚的。用于近似PI的最终迭代次数是通过命令行传递的-iterations-threads的数量的乘积。保留-threads空白会将其默认为1线程,从而导致在主线程中执行。

下面的测试总共进行了80万次迭代测试。

在Windows 7 64Bit(Intel Core2Duo Machine)上:

Windows Stats

使用Cygwin GCC 4.5.3编译:{{1​​}}

在Ubuntu / Linaro 12.04(8核心AMD)上:

Linux Stats

使用GCC 4.6.3编译:{{1​​}}

效果

在Windows上,线程版本比无线程版本快几毫秒。说实话,我期待更好的表现。在Linux上,哇!有没有搞错?为什么它甚至需要2000%的时间?当然,这在很大程度上取决于实现,所以在这里。完成命令行参数解析后的摘录并开始计算:

gcc-4 pi.c -o pi.exe -O3

gcc pi.c -lm -lpthread -O3 -o pi实现为:

    // Begin computation.
    clock_t t_start, t_delta;
    double pi = 0;

    if (args.threads == 1) {
        t_start = clock();
        pi = pi_mc(args.iterations);
        t_delta = clock() - t_start;
    }
    else {
        pthread_t* threads = malloc(sizeof(pthread_t) * args.threads);
        if (!threads) {
            return alloc_failed();
        }

        struct PIThreadData* values = malloc(sizeof(struct PIThreadData) * args.threads);
        if (!values) {
            free(threads);
            return alloc_failed();
        }

        t_start = clock();
        for (i=0; i < args.threads; i++) {
            values[i].iterations = args.iterations;
            values[i].out = 0.0;
            pthread_create(threads + i, NULL, pi_mc_threaded, values + i);
        }
        for (i=0; i < args.threads; i++) {
            pthread_join(threads[i], NULL);
            pi += values[i].out;
        }
        t_delta = clock() - t_start;

        free(threads);
        threads = NULL;
        free(values);
        values = NULL;

        pi /= (double) args.threads;
    }

您可以在http://pastebin.com/jptBTgwr找到完整的源代码。

问题

这是为什么?为什么Linux上存在这种极端差异?我预计计算的时间至少是原始时间的3/4。当然,我可能只是错误地使用了pi_mc_threaded()库。在这种情况下如何做正确的澄清将是非常好的。

3 个答案:

答案 0 :(得分:5)

问题是在glibc的实现中,rand()调用了__random(),而那

long int
__random ()
{
  int32_t retval;

  __libc_lock_lock (lock);

  (void) __random_r (&unsafe_state, &retval);

  __libc_lock_unlock (lock);

  return retval;
}

锁定对执行实际工作的函数__random_r的每次调用。

因此,只要您使用rand()有多个线程,就会使每个线程在几乎每次调用rand()时等待其他线程。在每个线程中直接使用random_r()和自己的缓冲区应该要快得多。

答案 1 :(得分:1)

性能和线程是一种黑色艺术。答案取决于用于执行线程的编译器和库的具体情况,内核处理它的程度等等。基本上,如果* nix的库在切换,移动对象等方面效率不高,那么线程实际上是慢点 。这是我们很多线程工作现在使用JVM或JVM类语言的原因之一。我们可以信任运行时JVM的行为 - 它的整体速度可能因平台而异,但它在该平台上是一致的。此外,您可能会遇到一些隐藏的等待/竞争条件,这些条件是由于Windows上可能未显示的时间而发现的。

如果你能够改变你的语言,可以考虑使用Scala或D. Scala是Java的演员驱动模型继承者,D是C的继承者。两种语言都显示了它们的根源 - 如果你能用C语言写的话,D应该没问题。但是,这两种语言都实现了actor模型。没有更多的线程池,没有更多的比赛条件等等!!!!!!

答案 2 :(得分:1)

为了比较,我刚刚在Windows Vista上尝试过使用Borland C ++编译的应用程序,而2线程版本的执行速度几乎是单线程的两倍。

pi.exe -iterations 20000000 -stats -threads 1
3.141167

Number of iterations:  20000000
Method:                Monte Carlo
Evaluation time:       12.511000 sec
Threads:               Main


pi.exe -iterations 10000000 -stats -threads 2
3.142397

Number of iterations:  20000000
Method:                Monte Carlo
Evaluation time:       6.584000 sec
Threads:               2

这是针对线程安全的运行时库编译的。使用单线程库,两个版本的运行速度都是线程安全速度的两倍。

pi.exe -iterations 20000000 -stats -threads 1
3.141167

Number of iterations:  20000000
Method:                Monte Carlo
Evaluation time:       6.458000 sec
Threads:               Main


pi.exe -iterations 10000000 -stats -threads 2
3.141314

Number of iterations:  20000000
Method:                Monte Carlo
Evaluation time:       3.978000 sec
Threads:               2

所以2线程版本的速度仍然是原来的两倍,但是带有单线程库的1线程版本实际上比线程安全库中的2线程版本更快。

看看Borland的rand实现,他们在线程安全实现中使用线程本地存储作为种子,因此它不会像glibc的锁一样对线程代码产生相同的负面影响,但是线程安全的实现显然是比单线程实现慢。

但最重要的是,编译器的rand实现可能是两种情况下的主要性能问题。

<强>更新

我刚尝试使用种子的局部变量替换你的rand_01调用Borland的rand函数的内联实现,结果在2线程情况下的结果始终是两倍。 / p>

更新的代码如下所示:

#define MULTIPLIER      0x015a4e35L
#define INCREMENT       1

double pi_mc(int iterations) {
    unsigned seed = 1;
    long long inner = 0;
    long long outer = 0;
    int i;
    for (i=0; i < iterations; i++) {
        seed = MULTIPLIER * seed + INCREMENT;
        double x = ((int)(seed >> 16) & 0x7fff) / (double) RAND_MAX;

        seed = MULTIPLIER * seed + INCREMENT;
        double y = ((int)(seed >> 16) & 0x7fff) / (double) RAND_MAX;

        double d = sqrt(pow(x, 2.0) + pow(y, 2.0));
        if (d <= 1.0) {
            inner++;
        }
        else {
            outer++;
        }
    }

    return ((double) inner / (double) iterations) * 4;
}

我不知道rand实现有多好,但至少值得尝试在Linux上查看它是否会对性能产生影响。