有线程的代码比没有线程的代码花费的时间更长
Code with threads taking longer than without
我正在用 C 语言试验线程,我对一些结果感到困惑。
我有以下循环:
for(size_t i=0;i<1000000000;i++){
a++;
}
它增加了一个全局变量a
。我用 6 个变量完成了此操作,a
到 e
。
首先,我在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 个字节只是随行。
但是,如果那些其他字节中的任何一个需要被另一个核心修改,它们必须使用缓存一致性协议来确保它们不会破坏彼此的内存。当一个缓存行被写入时,它会被标记为已修改,并且每个其他缓存都必须将其标记为无效并重新加载才能再次使用它。
当您在代码中将变量定义为:
size_t a,b,c,d,e,f;
它们都在内存中排成一个连续的块,最终会少于一个完整的缓存行。这意味着每个线程都在争夺一个 64 字节的内存块,并且在拥有它之前无法前进。这使得实际执行是串行的,即使多个内核可能同时执行代码。
以下是我对 运行 程序的结果:(测试是您的第一个代码示例,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
速度更快!
我正在用 C 语言试验线程,我对一些结果感到困惑。
我有以下循环:
for(size_t i=0;i<1000000000;i++){
a++;
}
它增加了一个全局变量a
。我用 6 个变量完成了此操作,a
到 e
。
首先,我在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 个字节只是随行。
但是,如果那些其他字节中的任何一个需要被另一个核心修改,它们必须使用缓存一致性协议来确保它们不会破坏彼此的内存。当一个缓存行被写入时,它会被标记为已修改,并且每个其他缓存都必须将其标记为无效并重新加载才能再次使用它。
当您在代码中将变量定义为:
size_t a,b,c,d,e,f;
它们都在内存中排成一个连续的块,最终会少于一个完整的缓存行。这意味着每个线程都在争夺一个 64 字节的内存块,并且在拥有它之前无法前进。这使得实际执行是串行的,即使多个内核可能同时执行代码。
以下是我对 运行 程序的结果:(测试是您的第一个代码示例,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
速度更快!