确定最佳线程数

7
作为学校任务的一部分,我们需要构建一个玩具程序来确定我们个人计算机的最佳线程数。首先,我们需要创建一个需要运行20到30秒的任务。我选择了进行硬币抛掷模拟,将累积得到的正反面总数显示出来。在我的机器上,单线程3亿次抛掷耗时25秒。之后,我尝试使用2线程、4线程、8线程、16线程、32线程以及仅供娱乐使用的100线程进行测试。以下是测试结果:
* 线程数 抛掷次数 时间(秒) * ------------------------------------------ * 1 300,000,000 25 * 2 150,000,000 13 * 4 75,000,000 13 * 8 37,500,000 13 * 16 18,750,000 14 * 32 9,375,000 14 * 100 3,000,000 14
这是我所使用的代码:
void toss()
{
    int heads = 0, tails = 0;
    default_random_engine gen;
    uniform_int_distribution<int> dist(0,1);
    int max =3000000;                          //tosses per thread
    for(int x = 0; x < max; ++x){(dist(gen))?++heads:++tails;}
    cout<<heads<<" "<<tails<<endl;
}

int main()
{
    vector<thread>thr;
    time_t st, fin;
    st = time(0);

    for(int i = 0;i < 100;++i){thr.push_back(thread(toss));} //thread count
    for(auto& thread: thr){thread.join();}

    fin = time(0);
    cout<<fin-st<<" seconds\n";
    return 0;
}

现在是主要问题:
超过某个点后,随着添加更多线程,我本来期望计算速度会显著下降,但结果似乎并没有表现出这一点。
我的代码有根本性的错误导致这种结果吗?还是这种行为被认为是正常的?我对多线程非常新手,所以我感觉可能是前者……
谢谢!
编辑:我正在MacBook上运行,使用2.16 GHz Core 2 Duo (T7400)处理器

1
“我本来预计随着添加更多线程,计算速度会显著下降,为什么?你是假设实现会有很多上下文切换,这些切换会占用执行时间的大部分吗?只有在实现没有选择(例如线程一直阻塞)的情况下才会发生这种情况,但是在这里它有选择,它可以让线程运行。如果您有一个不错的操作系统,设计其调度程序的人员都是世界级专家,当有更好的选择时,他们不会做明显的错误。” - David Schwartz
2个回答

10

你的结果看起来非常正常。虽然创建线程是有代价的,但它并不是那么昂贵(特别是与您测试的每秒时间粒度相比)。我打赌额外的100个线程的创建、销毁和可能的上下文切换不会使您的计时多出几毫秒。

在我的Intel i7-4790 @ 3.60 GHz上运行,我得到了以下数字:

threads - seconds
-----------------
1       -  6.021
2       -  3.205
4       -  1.825
8       -  1.062
16      -  1.128
32      -  1.138
100     -  1.213
1000    -  2.312
10000   - 23.319

需要很多很多的线程才能到达额外的线程产生显著差异的程度。只有当线程数达到1,000个时,我才看到线程管理产生了显著差异,而在10,000个时,它超过了循环(此时循环仅执行30,000次抛掷)。

对于你的任务,很容易看出你的系统的最佳线程数应该与可以同时执行的可用线程数相同。直到一个线程完成或放弃,否则没有剩余的处理能力来执行另一个线程,这并不能帮助你更快地完成。任何少于线程数的情况都没有使用所有可用资源。我的 CPU 有8个线程,图表反映了这一点。


4

编辑2 - 针对“缺乏性能惩罚”的进一步阐述:

……我本来预计随着添加更多线程,计算速度会显著下降,但结果似乎并未显示出这一点。

我制作了这个巨大的图表以更好地说明扩展情况。

enter image description here

为了解释结果:
蓝色条表示完成所有投掷所需的总时间。虽然该时间一直减少到256个线程,但是每次加倍线程数量的收益越来越小。我运行这个测试的CPU有4个物理核心和8个逻辑核心。从1个核心扩展到4个核心的效果非常好,扩展到8个核心的效果也不错,但之后就急剧下降。管道饱和度允许在256个线程之前获得微小的增益,但这根本不值得。
红色条表示每次投掷所需的时间。对于1和2个线程,它几乎相同,因为CPU管道尚未达到完全饱和状态。它在4个线程处受到轻微影响,但仍然可以正常运行,现在管道已经饱和,在8个线程处真正显示出逻辑线程与物理线程不同,这会随着线程数的增加而逐渐恶化。
绿色条形图显示了开销,即实际性能相对于预期的双倍提升下降了多少。超过可用逻辑核心会导致开销激增。请注意,这主要是线程同步问题,实际线程调度开销在一定点之后可能是恒定的,线程必须接收最小活动时间窗口,这就解释了为什么线程切换不会压倒工作吞吐量。事实上,一直到4k线程,没有严重的性能下降,这是因为现代系统必须能够并行运行数千个线程。而且,大部分下降都是由线程同步引起的,而不是线程切换。
黑色轮廓条显示了相对于最低时间的时间差异。在8个线程下,由于管道未过度饱和,我们只失去了约14%的绝对性能,这是一件好事,因为在大多数情况下,不值得为了如此少的东西而使整个系统紧张。它还表明,1个线程仅比CPU可以完成的最大值慢约6倍。这给出了逻辑核心相对于物理核心的好处,100%额外的逻辑核心可提高50%的性能,在这种用例中,逻辑线程约为物理线程的50%,这也与我们从4到8看到的约47%的提升相一致。请注意,这是一个非常简单的工作负载,在更苛刻的情况下,对于这个特定的CPU,接近20-25%,在某些边缘情况下实际上会影响性能。

编辑1-我愚蠢地忘记了将计算工作负载与线程同步工作负载隔离。

运行测试时,很少或没有工作显示出对于高线程数,线程管理部分占据大部分时间。因此,线程切换惩罚确实非常小,可能在某一点后是恒定的。

如果你把自己放在一个线程调度器制造者的位置上,这将非常有意义。调度程序可以轻松地避免被不合理高的切换到工作比率所窒息,因此,在切换到另一个线程之前,调度程序可能会给一个线程最小的时间窗口,而其余的线程则被放置在等待中。这确保了切换到工作比率永远不会超过合理范围。相比进行疯狂的线程切换,将其他线程停顿会更好,因为CPU主要是在切换和做很少的实际工作。


最佳线程数是可用的逻辑CPU核心数量。这样可以实现最佳的流水线饱和度。
如果使用更多线程,由于线程上下文切换的成本,性能会降低。线程越多,惩罚就越大。
如果使用更少线程,则无法充分利用硬件潜力。
还有工作负载颗粒度的问题,当您使用诸如互斥锁之类的同步时,这非常重要。如果并发性过于细粒度,则即使在8线程机器上从1个线程增加到2个线程时,也可能会出现性能下降。您需要尽可能减少同步,在同步之间尽可能多地完成工作,否则可能会出现巨大的性能下降。
请注意物理和逻辑CPU核心之间的区别。具有超线程的处理器可以在每个物理核心上拥有多个逻辑核心。 "次要"逻辑核心与“主要”逻辑核心的计算能力不同,因为它们仅用于利用处理器管道使用中的空缺。
例如,如果您有一个4核8线程的CPU,在完美扩展的工作负载情况下,从1个线程增加到4个线程将看到性能增加4倍,但从4个线程增加到8个线程的性能增加要少得多,正如vu1p3n0x的回答所示。
您可以查看此处以了解确定可用CPU核心数量的方法。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接