【问题标题】:Why are 50 threads faster than 4?为什么 50 个线程比 4 个线程快?
【发布时间】:2013-04-28 22:05:59
【问题描述】:
DWORD WINAPI MyThreadFunction(LPVOID lpParam) {
    volatile auto x = 1;
    for (auto i = 0; i < 800000000 / MAX_THREADS; ++i) {
        x += i / 3;
    }
    return 0;
}

这个函数在 MAX_THREADS 线程中运行。
我已经在 Intel Core 2 DuoWindows 7MS Visual Studio 上运行了测试2012 使用 Concurrency Visualizer 和 MAX_THREADS=4MAX_THREADS=50
test1(4 个线程)在 7.1 秒内完成,但 test2(50 个线程)在
strong>5.8 秒,而 test1 的上下文切换比 test2 多。
我在 Intel Core i5Mac OS 10.7.5 上运行了相同的测试并得到了相同的结果。

【问题讨论】:

  • 你试过 4 到 50 个线程之间的任何东西吗?您使用了哪些编译器选项?
  • 你的处理器有多少个实际内核(我已经有一段时间没有查看不同型号的英特尔处理器了 - 我是一个 AMD 人,大约 20 年前,还没有我自己的电脑当时有英特尔处理器)?你如何测量时间?
  • 您能否描述一下您是如何设置测试的以及您在哪个阶段开始计时的?我希望您创建了所有暂停的线程,然后启动您的计时器并立即恢复所有线程。然后,您应该在所有线程都发出信号表明它们已完成后停止计时器。运行程序比几秒钟更长时间以获得真正的测量值是值得的。和/或多次重复实验。
  • 我只是使用默认选项创建控制台应用程序项目。但在 Mac OS 中,我使用带有 -O3 的 GCC。
  • @paddy 我在程序启动时启动计时器并在完成前停止。但在 MSVC 中,我使用并发可视化工具而不是计时器。我试过很多次了。

标签: c++ multithreading cpu intel


【解决方案1】:

我决定自己在我的 4 核机器上进行基准测试。我通过对每个线程进行 100 次测试,直接比较了 4 个线程和 50 个线程。我使用自己的数字,以便为每个任务有合理的执行时间。

结果如您所描述。 50 线程版本稍微快一些。这是我的结果的箱线图:

为什么?我认为这归结为线程调度。直到所有线程都完成了工作,任务才完成,每个线程必须完成四分之一的工作。因为你的进程正在与系统上的其他进程共享,如果任何单个线程切换到另一个进程,这将延迟整个任务。当我们等待最后一个线程完成时,所有其他内核都处于空闲状态。请注意,4 线程测试的时间分布比 50 线程测试的时间分布要宽得多,这是我们可能预料到的。

当您使用 50 个线程时,每个线程的工作量就会减少。因此,单个线程中的任何延迟对总时间的影响都不那么显着。当调度程序忙于将内核分配给许多短线程时,可以通过给这些线程在另一个内核上的时间来补偿一个内核上的延迟。延迟对一个内核的总体影响并没有那么大。

因此,在这种情况下,额外的上下文切换似乎并不是最大的因素。虽然增益很小,但考虑到处理比上下文切换更重要,稍微占用线程调度程序似乎是有益的。与所有事情一样,您必须为您的应用找到正确的平衡点。


[edit] 出于好奇,我一夜之间运行了一个测试,而我的计算机并没有做太多其他事情。这次我每次测试使用 200 个样本。同样,测试是交错的,以减少任何本地化后台任务的影响。

这些结果的第一个图是针对低线程数(最多为内核数的 3 倍)。你可以看到一些线程数的选择是多么的糟糕......也就是说,任何不是核心数的倍数,尤其是奇数值。

第二个图适用于更高的线程数(从 3 倍内核数到 60)。

在上面,随着线程数的增加,您可以看到明显的下降趋势。随着线程数的增加,您还可以看到结果的分布范围缩小。

在这个测试中,有趣的是,4 线程和 50 线程测试的性能大致相同,并且 4 核测试中结果的分布没有我原来的测试那么广泛。因为计算机没有做太多其他事情,所以它可以花时间进行测试。将一个核心置于 75% 负载下重复测试会很有趣。

为了让事情保持正确,考虑一下:


[另一个编辑]在发布了我最后一批结果后,我注意到混乱的箱形图显示了那些测试是 4 的倍数的趋势,但数据有点难以看到.

我决定只用四的倍数做一个测试,并认为我不妨同时找到收益递减点。所以我使用了 2 次方的线程数,最高可达 1024。我本来会更高,但 Windows 在大约 1400 个线程时出错了。

我认为结果相当不错。如果您想知道小圆圈是什么,这些是中值。我选择它而不是我之前使用的红线,因为它更清楚地显示了趋势。

似乎在这种特殊情况下,支付污垢位于 50 到 150 个线程之间。在那之后,好处很快就消失了,我们正在进入过度线程管理和上下文切换的领域。

随着任务的延长或缩短,结果可能会有很大差异。在这种情况下,这是一项涉及大量无意义算术的任务,在单核上计算大约需要 18 秒。

通过仅调整线程数,我能够将 4 线程版本的中位执行时间额外减少 1.5% 到 2%。

【讨论】:

  • 感谢@Mysticial。实际上,我很失望我没有为更高的线程数选择更好的值。如果您只查看 4 的倍数的箱线图,它会形成一条非常漂亮的渐近曲线。但是我没有测试所有 4 到 60 的倍数。将测试扩展到疯狂的线程数(如 10000)以查看性能下降的地方会很有趣。测量它可能会很痛苦,因为我一次最多只能等待 64 个线程句柄。我必须确保对WaitForMultipleObjects 的多次调用不会影响结果。
  • @paddy:你恢复了我对 Stack Overflow 的信心。
  • 非常感谢!你如何打印这些图表?是自制的吗?看起来像 Matlab。
  • 不用担心。深入研究它是非常有趣的。我认为还有很多关于这个的话题。这些图表确实是使用 Matlab 使用 boxplot 和几乎默认设置完成的。
  • 最后,一些理智和实际的数字,而不是通常的教科书/FUD 口头禅“只产生与 CPU 密集型工作的核心一样多的线程,否则你将失去上下文的性能 -交换'。不过,公平地说,最佳线程数在很大程度上取决于正在操作的数据集——如果数据占用完整的 L1 缓存或更多,则统计数据确实会严重偏向每个内核一个线程。不过,对于实际尝试的东西 +1!
【解决方案2】:

这完全取决于您的线程在做什么

您的计算机只能同时运行与系统中的内核一样多的线程。这包括通过超线程等功能实现的虚拟内核。

CPU 绑定

如果您的线程受 CPU 限制(这意味着它们将大部分时间用于对内存中的数据进行计算),那么通过将线程数增加到内核数之上,您将几乎看不到任何改进。实际上,运行更多线程会失去效率,因为必须在 CPU 内核上和关闭线程上进行上下文切换会增加开销。

I/O 绑定

哪里 (#threads > #cores) 有帮助,当您的线程受 I/O 限制时,这意味着它们大部分时间都在等待 I/O,(硬盘、网络, 其他硬件等)在这种情况下,一个被阻塞等待 I/O 完成的线程将被从 CPU 中拉出,而实际上准备好做某事的线程将被放置。

获得最高效率的方法是始终让 CPU 忙于一个真正在做某事的线程。 (不等待某事,也不上下文切换到其他线程。)

【讨论】:

  • 你没有错过问题的重点吗?他们期待您描述的行为,但实验结果却相反。
  • 查看他们的代码,看起来他们应该只受 CPU 限制,但他们看到更多的上下文切换,并且线程越少越慢。所以,他们的结果似乎与你的答案背道而驰。
  • @Xymotech 叹息,你是对的。我应该更仔细地阅读这个问题。我想我看到标题就开始回答了。
  • @JonathonReinhart 无论如何,谢谢,对于我过去遇到的许多其他线程问题,这是一个非常有启发性的答案。
【解决方案3】:

我将一些我“放置”的代码用于其他目的,然后重新使用它 - 所以请注意它不是“漂亮”,也不应该是你应该如何做的一个很好的例子。

这是我想出的代码(这是在 Linux 系统上,所以我使用 pthreads 并删除了“WINDOWS-isms”:

#include <iostream>
#include <pthread.h>
#include <cstring>

int MAX_THREADS = 4;

void * MyThreadFunction(void *) {
    volatile auto x = 1;
    for (auto i = 0; i < 800000000 / MAX_THREADS; ++i) {
        x += i / 3;
    }
    return 0;
}


using namespace std;

int main(int argc, char **argv)
{
    for(int i = 1; i < argc; i++)
    {
    if (strcmp(argv[i], "-t") == 0 && argc > i+1)
    {
        i++;
        MAX_THREADS = strtol(argv[i], NULL, 0);
        if (MAX_THREADS == 0)
        {
        cerr << "Hmm, seems like end is not a number..." << endl;
        return 1;
        }       
    }
    }
    cout << "Using " << MAX_THREADS << " threads" << endl;
    pthread_t *thread_id = new pthread_t [MAX_THREADS];
    for(int i = 0; i < MAX_THREADS; i++)
    {
    int rc = pthread_create(&thread_id[i], NULL, MyThreadFunction, NULL);
    if (rc != 0)
    {
        cerr << "Huh? Pthread couldn't be created. rc=" << rc << endl;
    }
    }
    for(int i = 0; i < MAX_THREADS; i++)
    {
        pthread_join(thread_id[i], NULL);
    }
    delete [] thread_id;
}

用不同数量的线程运行它:

MatsP@linuxhost junk]$ g++ -Wall -O3 -o thread_speed thread_speed.cpp -std=c++0x -lpthread
[MatsP@linuxhost junk]$ time ./thread_speed -t 4
Using 4 threads

real    0m0.448s
user    0m1.673s
sys 0m0.004s
[MatsP@linuxhost junk]$ time ./thread_speed -t 50
Using 50 threads

real    0m0.438s
user    0m1.683s
sys 0m0.008s
[MatsP@linuxhost junk]$ time ./thread_speed -t 1
Using 1 threads

real    0m1.666s
user    0m1.658s
sys 0m0.004s
[MatsP@linuxhost junk]$ time ./thread_speed -t 2
Using 2 threads

real    0m0.847s
user    0m1.670s
sys 0m0.004s
[MatsP@linuxhost junk]$ time ./thread_speed -t 50
Using 50 threads

real    0m0.434s
user    0m1.670s
sys 0m0.005s

如您所见,“用户”时间几乎保持不变。我实际上也尝试了很多其他值。但是结果是一样的,所以我不会再用十几个显示几乎相同的东西让你们感到厌烦。

这是在四核处理器上运行的,因此您可以看到“超过 4 个线程”时间显示的“实际”时间与“4 个线程”相同。

我非常怀疑 Windows 处理线程的方式有什么不同。

我还使用#define MAX_THREADS 50 编译了代码,并再次使用 4 编译了代码。它对发布的代码没有任何影响 - 但只是为了涵盖编译器优化代码的替代方案。

顺便说一句,我的代码运行速度快了三到十倍这一事实表明最初发布的代码使用的是调试模式?

【讨论】:

  • 是的,Windows 上的代码在调试模式下运行,在 Mac OS 上以优化模式运行。定量结果不同。但我只是想展示 4 和 50 个线程之间奇怪的性能差异。
【解决方案4】:

我不久前在 Windows(Vista 64 Ultimate)上进行了一些测试,在 4/8 核 i7 上。我使用了类似的“计数”代码,将任务作为任务提交到具有不同线程数的线程池,但总工作量始终相同。池中的线程被赋予低优先级,以便所有任务在线程和计时开始之前排队。显然,盒子在其他方面处于空闲状态(大约 1% 的 CPU 用于服务等)。

8 tests,
400 tasks,
counting to 10000000,
using 8 threads:
Ticks: 2199
Ticks: 2184
Ticks: 2215
Ticks: 2153
Ticks: 2200
Ticks: 2215
Ticks: 2200
Ticks: 2230
Average: 2199 ms

8 tests,
400 tasks,
counting to 10000000,
using 32 threads:
Ticks: 2137
Ticks: 2121
Ticks: 2153
Ticks: 2138
Ticks: 2137
Ticks: 2121
Ticks: 2153
Ticks: 2137
Average: 2137 ms

8 tests,
400 tasks,
counting to 10000000,
using 128 threads:
Ticks: 2168
Ticks: 2106
Ticks: 2184
Ticks: 2106
Ticks: 2137
Ticks: 2122
Ticks: 2106
Ticks: 2137
Average: 2133 ms

8 tests,
400 tasks,
counting to 10000000,
using 400 threads:
Ticks: 2137
Ticks: 2153
Ticks: 2059
Ticks: 2153
Ticks: 2168
Ticks: 2122
Ticks: 2168
Ticks: 2138
Average: 2137 ms

对于需要很长时间的任务,并且在上下文更改时换出的缓存很少,使用的线程数对整体运行时间几乎没有任何影响。

【讨论】:

    【解决方案5】:

    您遇到的问题与您细分流程工作负载的方式密切相关。为了在多任务操作系统上有效地使用多核系统,您必须确保在进程生命周期内尽可能长时间地为所有内核提供剩余工作。

    考虑您的 4 线程进程在 4 个内核上执行的情况,并且由于系统负载配置,其中一个内核的完成速度比其他内核快 50%:对于剩余的处理时间,您的 CPU 将只能将 3/4 的处理能力分配给您的进程,因为只剩下 3 个线程。在相同的 CPU 负载情况下,但有更多线程,工作负载被拆分为更多子任务,这些子任务可以在内核之间更精细地分配,所有其他条件都相同 (*)。

    这个例子说明,时间差异实际上并不是由于线程数量,而是由于工作分配的方式,这在后一种情况下对内核可用性不均的情况更具弹性。同一个程序只用 4 个线程构建,但工作被抽象为一系列由线程拉取的小任务,一旦它们可用,肯定会产生类似甚至更好的平均结果,即使会有管理的开销任务队列。

    流程任务集更精细的粒度使其具有更好的灵活性。


    (*) 在高负载系统的情况下,多线程方法可能没有那么有用,未使用的内核实际上被分配给其他操作系统进程,因此减轻了您仍然可能使用的其他三个内核的负载过程。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2021-10-12
      • 1970-01-01
      • 2016-10-29
      • 2013-11-22
      • 1970-01-01
      • 1970-01-01
      • 2011-01-03
      • 2015-08-22
      相关资源
      最近更新 更多