【问题标题】:multi-threading not improving performance in recursive c++ program多线程不会提高递归 C++ 程序的性能
【发布时间】:2013-09-06 16:08:43
【问题描述】:

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

#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 的一些计时:

NUMTHREADS = 1:

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

NUMTHREADS = 2:

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

NUMTHREADS = 3:

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

NUMTHREADS = 4:

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

知道为什么会这样吗?

【问题讨论】:

  • 您的假设是错误的,开销很大。在没有优化的情况下比较性能几乎没有意义,几乎任何基本的优化器都会用一条指令替换那个微不足道的循环。
  • 为什么在没有优化的情况下比较性能毫无意义?我没有使用 -O2 并且时间反映循环仍在执行中。使用 -O2 执行 4 个线程大约需要 0.023 秒。因此,创建和加入 4 个线程不会产生 1.5 秒的开销。
  • -O2 改变了很多东西,而不仅仅是循环。您还可能遇到false sharing 问题,每个 CPU 都在更新不同的位置,这些位置都落入同一缓存行;所以你投入工作的核心越多,它就越慢。尝试分散g 的元素,使它们至少相隔64 个字节,看看是否有任何变化。即使您处理可能的缓存问题,您仍然会受到 RAM 带宽的限制(尤其是您的代码可能会受到内存 I/O 限制)。
  • 你是对的,这是虚假分享。除了用虚拟变量手动填充代码之外,有没有办法避免这种情况?
  • 太好了,我改变了循环,所以它可以与堆栈上的变量一起使用并且它是固定的。

标签: c++ concurrency


【解决方案1】:

由于在访问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 缓存或主内存。由于共享的最后一级缓存和主内存都比每个内核的私有缓存慢得多,这会导致执行速度慢得多。

修改后的版本将g中的索引乘以16:

|     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)并且在内存页面边界上对齐,因此不同线程中的堆栈变量永远不会共享相同的缓存行。此外,编译器还进一步优化了对堆栈变量的访问。

请参阅this answer 以获得更彻底的解释以及防止虚假共享的另一种方法。虽然它是关于 OpenMP,但这些概念也适用于您的案例。

【讨论】:

    【解决方案2】:

    你应该:

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

    2. 声明变体gvolatile,否则编译时可能会被优化掉。

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

    【讨论】:

    • 将 g 声明为 volatile 将导致程序花费更多时间,因为优化将被删除。无论如何,他并没有从两个差异线程修改相同的内存。
    • 使用 -O2,循环将被优化为只有 1 条指令.. 因此程序将立即为任何少量线程运行
    • @rahul.deshmukhpatil volatile 通常用于banchmark,因为实际程序很难优化您的变体,对于banchmark,您还应该使变体不优化。
    • @user816318, volatile 使其未优化。
    • 请参考en.wikipedia.org/wiki/…,上面说优化将被删除,因此可能需要更多时间来执行带有易失变量的代码。
    【解决方案3】:

    我相信在创建和加入这样的线程时会有很多开销(请参阅这个问题C++11: std::thread pooled?)。如果您想通过并行化提高效率,请查看 OpenMP 之类的东西。

    【讨论】:

    • 我相信这不是线程开销,因为我用不同的循环迭代次数测试了程序。使用 10 亿而不是 1 亿,每个时间大致乘以 10。
    【解决方案4】:

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

    所以如果线程 1,2,3,4 分配给 diff cores 1 2 3 4(不要使用 0),那么所有的都会同时增加索引。请参阅$cpuinfo 以查看处理器的内核。并使用thread-&gt;setAffinity(core_numer); 设置线程的核心。

    【讨论】:

    • 手动设置亲和力不是这里的解决方案,除非特殊情况,否则应避免。绝对不应该盲目地希望它会改变事情,调度程序可能会比你做得更好。 std::thread 没有 setAffinity 成员。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-05-18
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多