2

考虑这个递归多线程程序:

#include <iostream>
#include <thread>

#define NUMTHREADS 4
using namespace std;

int g[NUMTHREADS];

thread t[NUMTHREADS];

void task1(int x)
{
    if(x+1<NUMTHREADS)
        t[x] = thread(task1, x+1);
    for(int i=0;i<100000000;i++)
        g[x]++;
    if(x+1<NUMTHREADS)
        t[x].join();
}

int main()
{
    task1(0);
    for(int i=0;i<NUMTHREADS;i++)
        cout<<g[i]<<" ";
}

我期望线程开销微不足道,但实际上程序的运行时间随着线程数线性增加。

以下是我的 6 核 cpu 的一些计时:

线程数 = 1:

$ time ./a
100000000
real    0m0.330s
user    0m0.312s
sys     0m0.015s

线程数 = 2:

$ time ./a
100000000 100000000
real    0m0.742s
user    0m1.404s
sys     0m0.015s

线程数 = 3:

$ time ./a
100000000 100000000 100000000
real    0m1.038s
user    0m2.792s
sys     0m0.000s

线程数 = 4:

$ time ./a
100000000 100000000 100000000 100000000
real    0m1.511s
user    0m5.616s
sys     0m0.015s

知道为什么会这样吗?

4

4 回答 4

5

由于在访问g. 这是您的程序的修改版本,它可以避免错误共享并使用不同数量的线程运行相同的时间,只要每个线程可以分配给不同的 CPU 内核:

#include <iostream>
#include <thread>

#define NUMTHREADS 4
using namespace std;

int g[NUMTHREADS*16];

thread t[NUMTHREADS];

void task1(int x)
{
    if(x+1<NUMTHREADS)
        t[x] = thread(task1, x+1);
    for(int i=0;i<100000000;i++)
        g[x*16]++;
    if(x+1<NUMTHREADS)
        t[x].join();
}

int main()
{
    task1(0);
    for(int i=0;i<NUMTHREADS;i++)
        cout<<g[i*16]<<" ";
}

1个和4个线程的运行时间:

$ time ./a.out
100000000
./a.out  0.45s user 0.01s system 98% cpu 0.466 total
                                         ^^^^^^^^^^^
$ time ./a.out
100000000 100000000 100000000 100000000
./a.out  1.52s user 0.01s system 329% cpu 0.462 total
                                          ^^^^^^^^^^^

这里是对发生的事情的简短解释。现代 x86 CPU 以 64 字节的块访问主内存,称为高速缓存行(除非使用非临时存储或加载指令,但这里不是这种情况)。该大小的单个高速缓存行最多可以容纳一个int数组的 16 个元素:

|    single cache line      |  another cache line
|------+------+-----+-------|-------+-------+------
| g[0] | g[1] | ... | g[15] | g[16] | g[17] | ...
+------+------+-----+-------+-------+-------+------
    ^      ^
    |      |
    |      +------ thread 1 updates this element
    |
    +------------- thread 0 updates this element

x86 是一个高速缓存一致性架构,这意味着当一个高速缓存行在单个核心中被修改时,其他核心会被告知其同一高速缓存行的副本不再有效,必须从上层内存存储重新加载,例如共享的 L3 高速缓存或主存储器。由于共享的最后一级缓存和主内存都比每个内核的私有缓存慢得多,这会导致执行速度慢得多。

修改后的版本将索引乘以g16:

|     one cache line        |  another cache line
|------+------+-----+-------|-------+-------+------
| g[0] | g[1] | ... | g[15] | g[16] | g[17] | ...
+------+------+-----+-------+-------+-------+------
    ^                           ^
    |                           |
    |                           +------ thread 1 updates this element
    |
    +------------- thread 0 updates this element

现在很清楚,没有两个线程共享相同的高速缓存行,因此高速缓存一致性协议不参与该过程。

使用堆栈变量时也可以达到同样的效果。线程堆栈通常很大(至少几个 KiB)并且在内存页面边界上对齐,因此不同线程中的堆栈变量永远不会共享相同的缓存行。此外,编译器还进一步优化了对堆栈变量的访问。

请参阅此答案以获得更彻底的解释以及防止错误共享的另一种方法。尽管它与 OpenMP 有关,但这些概念也适用于您的情况。

于 2013-09-06T08:08:43.377 回答
-1

I believe there is quite a lot of overhead in creating and joining threads like that (see this question C++11: std::thread pooled?). Check out something like OpenMP if you want to parallelize for efficiency.

于 2013-09-06T05:26:44.733 回答
-1

线程在彼此并行的单独内核上运行时会提高性能。因此,通过将线程关联设置到不同的核心,将每个线程绑定到不同的核心。它可能是您的线程在单核上运行。

因此,如果将线程 1、2、3、4 分配给 diff cores 1 2 3 4(不要使用 0),那么所有这些都将同时增加索引。查看$cpuinfo您的处理器的核心。并用于thread->setAffinity(core_numer);设置线程的核心。

于 2013-09-06T05:21:19.757 回答
-1

你应该:

  1. 至少使用 -O2 优化编译您的代码。

  2. 将变量声明gvolatile,否则在编译时可能会被优化。

在我的 2 核机器上,thread=1 和 thread=2 的成本几乎相同。

于 2013-09-06T05:21:46.863 回答