23
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 VisualizerMAX_THREADS=4和运行了测试MAX_THREADS=50
test1(4 个线程)在7.1 秒内完成,但test2(50 个线程)在5.8 秒内完成,而test1上下文切换比test2.
我在Intel Core i5Mac OS 10.7.5上运行了相同的测试并得到了相同的结果。

4

5 回答 5

46

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

结果和你描述的一样。50 线程版本稍微快一些。这是我的结果的箱线图:

并行任务对比图

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

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

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


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

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

附加测试图 - 低线程数

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

附加测试图 - 高线程数

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

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

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

缩放线程


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

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

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

对线程数求幂的趋势

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

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

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

于 2013-04-29T02:47:03.580 回答
3

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

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

CPU 密集型

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

I/O 绑定

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

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

于 2013-04-28T22:07:48.917 回答
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 处理线程的方式有什么不同。

我还用 a#define MAX_THREADS 50和 4 再次编译了代码。它与发布的代码没有区别 - 但只是为了涵盖编译器优化代码的替代方案。

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

于 2013-04-28T23:25:40.093 回答
2

不久前,我在 4/8 核 i7 上的 Windows(Vista 64 Ultimate)上进行了一些测试。我使用了类似的“计数”代码,将任务作为任务提交到具有不同线程数的线程池,但总工作量始终相同。池中的线程被赋予低优先级,以便所有任务在线程和计时开始之前排队。显然,盒子是空闲的,(大约 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

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

于 2013-04-29T03:06:24.867 回答
0

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

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

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

流程任务集的更细粒度为其提供了更好的灵活性。


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

于 2013-04-29T05:37:11.427 回答