【问题标题】:pthread is slower than the "default" versionpthread 比“默认”版本慢
【发布时间】:2021-01-01 07:44:58
【问题描述】:

情况

我想看看使用pthread 的优势。如果我没记错的话:线程允许我并行执行程序的给定部分。

这就是我试图完成的事情:我想制作一个程序,它接受一个数字(比如说n)并输出[0..n] 的总和。

代码

#define MAX 1000000000

int
main() {
    long long n = 0;
    for (long long i = 1; i < MAX; ++i)
        n += i;

    printf("\nn: %lld\n", n);
    return 0;
}

时间:0m2.723s

据我了解,我可以简单地将那个数字 MAX 除以 2 并让 2 threads 做好工作。

代码

#define MAX          1000000000
#define MAX_THREADS  2
#define STRIDE       MAX / MAX_THREADS

typedef struct {
    long long off;
    long long res;
} arg_t;

void*
callback(void *args) {
    arg_t *arg = (arg_t*)args;

    for (long long i = arg->off; i < arg->off + STRIDE; ++i)
        arg->res += i;

    pthread_exit(0);
}

int
main() {
    pthread_t threads[MAX_THREADS];
    arg_t     results[MAX_THREADS];

    for (int i = 0; i < MAX_THREADS; ++i) {
        results[i].off = i * STRIDE;
        results[i].res = 0;

        pthread_create(&threads[i], NULL, callback, (void*)&results[i]);
    }

    for (int i = 0; i < MAX_THREADS; ++i)
        pthread_join(threads[i], NULL);

    long long result;
    result = results[0].res;

    for (int i = 1; i < MAX_THREADS; ++i)
        result += results[i].res;

    printf("\nn: %lld\n", result);

    return 0;
}

时间:0m8.530s

问题

pthread 的版本运行速度较慢。从逻辑上讲,这个版本应该运行得更快,但创建线程的成本可能更高。

有人可以提出解决方案或说明我在做什么/理解错误吗?

【问题讨论】:

  • 一个体面的编译器甚至可以将第一个循环优化为单个赋值。
  • 永远记得对优化的构建进行基准测试。
  • @Someprogrammerdude 你能推荐一个用于 C(操作系统:linux)的分析器吗?

标签: c multithreading pthreads


【解决方案1】:

你的问题是缓存抖动加上缺乏优化(我敢打赌你在没有它的情况下编译)。

天真的 (-O0) 代码

for (long long i = arg->off; i < arg->off + STRIDE; ++i)
    arg->res += i;

将访问*arg 的内存。以这种方式定义您的 results 数组后,该内存非常接近下一个 arg 的内存,并且两个线程将争夺相同的缓存线,从而使 RAM 缓存非常无效。

如果您使用 -O1 进行编译,则循环应改为使用寄存器,并且只在最后写入内存。然后,您应该通过线程获得更好的性能(gcc 上更高的优化级别似乎可以完全优化循环)

另一个(更好的)选择是在缓存行上对齐arg_t

typedef struct {
    _Alignas(64) /*typical cache line size*/ long long off;
    long long res;
} arg_t;

那么无论你是否开启优化,你都应该通过线程获得更好的性能。

良好的缓存利用率通常在多线程编程中非常重要(Ulrich Drepper 在他臭名昭著的What Every Programmer Should Know About Memory 中对这个话题有很多话要说)。

【讨论】:

  • 感谢您的信息。我尝试了优化标志...是的,它们的运行速度明显更快,但是第二个版本比第一个版本慢一些。
  • @Hrant 这是我的测试/基准测试脚本(当前目录中的clobbers thr.c 和a.out):pastebin.com/f8C9B1Hr。我得到的结果与我在答案中写的一致。
  • @Hrant 如果您使用 time 二进制文件而不是 bash 内置函数,请小心。二进制文件默认首先报告用户时间(所有 CPU 内核上所有用户时间的总和)。经过的时间是第三个。
  • 啊,很高兴知道,感谢您在这里投入的时间。
【解决方案2】:

创建一大堆线程不太可能比简单地添加数字更快。 CPU 可以在内核建立和拆除线程所需的时间内添加大量整数。要看到多线程的好处,您确实需要每个线程都执行一项重要的任务——无论如何,与创建线程的开销相比是重要的。或者,您需要保持线程池运行,并根据某种分配策略分配它们工作。

当应用程序包含一些独立的任务时,多线程最有效,否则这些任务将相互等待完成。这不是获得更多吞吐量的神奇方法。

【讨论】:

  • 计算大约需要 3 秒。我猜它对线程来说还不够重要?
  • 我想你可以做一些分析,并尝试确定它是否存在。至少在 Linux 上,线程设置与进程设置大致相同——与整数运算相比,这是一项相当繁重的工作。
  • 感谢您的澄清。我会在 10 分钟后接受你的回答。
  • @KevinBoone,如果您忽略创建线程的成本(即在开始时创建一个池并根据需要重用它),那么将您的数组分成 4 个部分并在单独线程上的每个部分实际上都会看到实时长度的改进。
  • @Hrant 并不重要。系统线程需要几微秒才能启动,这对于现代 CPU 来说是很多时间,但与整秒相比,这种开销相形见绌。如果使用得当,线程应该会产生更好的速度。
猜你喜欢
  • 1970-01-01
  • 2011-09-15
  • 1970-01-01
  • 2017-08-05
  • 2019-07-17
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多