带线程的代码比没有线程花费的时间更长

标签 c multithreading pthreads

我正在用 C 语言试验线程,但我对某些结果感到困惑。
我有以下循环:

for(size_t i=0;i<1000000000;i++){
  a++;
}
它增加一个全局变量 a .我用 6 个变量完成了这个,ae .
首先,我在 main 中连续增加变量。 :
#include<stdio.h>
#include<pthread.h>

size_t a,b,c,d,e,f;

int main(void){
  for(size_t i=0;i<1000000000;i++){
    a++;
  }
  for(size_t i=0;i<1000000000;i++){
    b++;
  }
  for(size_t i=0;i<1000000000;i++){
    c++;
  }
  for(size_t i=0;i<1000000000;i++){
    d++;
  }
  for(size_t i=0;i<1000000000;i++){
    e++;
  }
  for(size_t i=0;i<1000000000;i++){
    f++;
  }
  size_t abcdef=a+b+c+d+e+f;
  printf("%zu\n",abcdef);
  return 0;
}
然后,当用 time 测试程序时,得到以下结果:
6000000000

real    0m11.450s
user    0m11.446s
sys     0m0.000s
我希望使用 pthreads 的结果会快很多:
#include<stdio.h>
#include<pthread.h>

size_t a,b,c,d,e,f;

void *t1(void *args){
  for(size_t i=0;i<1000000000;i++){
    a++;
  }
  return NULL;
}

void *t2(void *args){
  for(size_t i=0;i<1000000000;i++){
    b++;
  }
  return NULL;
}

void *t3(void *args){
  for(size_t i=0;i<1000000000;i++){
    c++;
  }
  return NULL;
}

void *t4(void *args){
  for(size_t i=0;i<1000000000;i++){
    d++;
  }
  return NULL;
}

void *t5(void *args){
  for(size_t i=0;i<1000000000;i++){
    e++;
  }
  return NULL;
}

void *t6(void *args){
  for(size_t i=0;i<1000000000;i++){
    f++;
  }
  return NULL;
}

int main(void){
  pthread_t p1,p2,p3,p4,p5,p6;
  pthread_create(&p1,NULL,t1,NULL);
  pthread_create(&p2,NULL,t2,NULL);
  pthread_create(&p3,NULL,t3,NULL);
  pthread_create(&p4,NULL,t4,NULL);
  pthread_create(&p5,NULL,t5,NULL);
  pthread_create(&p6,NULL,t6,NULL);
  pthread_join(p1,NULL);
  pthread_join(p2,NULL);
  pthread_join(p3,NULL);
  pthread_join(p4,NULL);
  pthread_join(p5,NULL);
  pthread_join(p6,NULL);
  size_t abcdef=a+b+c+d+e+f;
  printf("%zu\n",abcdef);
  return 0;
}
然而,结果却出乎意料:
6000000000

real    0m14.521s
user    1m26.048s
sys     0m0.014s
不仅实际时间更大,我预计会更低,而且用户时间超过 1 分钟,我没有等一分钟。
这里发生了什么?我该如何解决?

最佳答案

您在这里遇到的问题是由于缓存一致性。
在现代处理器中,单个内核一次可以访问的实际最小内存量是一个完整的高速缓存行,在许多现代处理器上是 64 字节。这意味着每个变量的每次增量都会读取 64 个字节,其中 8 个字节会针对增量进行修改。其他 56 个字节只是随手可用。
但是,如果其他任何字节需要由另一个内核修改,它们必须使用缓存一致性协议(protocol)来确保它们不会破坏彼此的内存。当写入缓存行时,它将被标记为已修改,并且每个其他缓存都必须将其标记为无效并重新加载以再次使用它。
当您在代码中将变量定义为:

size_t a,b,c,d,e,f;
它们都作为一个连续的 block 在内存中排列,最终将小于一个完整的高速缓存行。这意味着每个线程都在争夺一个 64 字节的内存块,并且在获得它之前无法继续前进。这使得实际执行是串行的,即使多个内核可能同时执行代码。
这是我运行程序的结果:(test 是您的第一个代码示例,test1 是 pthreads 示例)
$ time ./test
6000000000

real    0m22.526s
user    0m22.391s
sys     0m0.000s

$ time ./test1
6000000000

real    0m13.094s
user    1m7.797s
sys     0m0.047s
我的 pthreads 测试实际上更快。我怀疑这是由于我的 CPU 使用了超线程,它实际上在同一个内核上运行两个线程,它们共享同一个缓存行,所以没有争用。
我修改了 pthreads 代码以使用编译器指令使全局变量 64 字节对齐,这会强制每个变量位于其自己的缓存行中。
size_t a __attribute__ ((aligned (64)));
size_t b __attribute__ ((aligned (64)));
size_t c __attribute__ ((aligned (64)));
size_t d __attribute__ ((aligned (64)));
size_t e __attribute__ ((aligned (64)));
size_t f __attribute__ ((aligned (64)));
结果如下:
$ time ./test2
6000000000

real    0m2.665s
user    0m15.281s
sys     0m0.016s
它的速度更快!

关于带线程的代码比没有线程花费的时间更长,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/68005531/

相关文章:

c++ - 类型 'const char*' 和 'const char [93]' 的二进制操作数无效

c - 为什么我的子进程没有给我 "correct"结果?

使用线程的 Pythonic 方式

c++ - 线程性能基准测试

c++ - 封装线程会产生问题

C - 一个程序可以为自己分配多少内存 - 它是如何确定的?

c - 求和中每个单独项的除法公式

c# - Wpf 应用程序卡在主线程上,没有明显的锁

c# - Monitor.Wait() 和 Monitor.Pulse() 的线程问题

c - 不使用标志变量的线程同步