为什么50个线程比4个快?

时间:2013-04-28 22:05:59

标签: c++ multithreading cpu intel

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 Duo Windows 7 上运行测试MS Visual Studio 2012 使用并发可视化工具MAX_THREADS=4MAX_THREADS=50
test1(4个主题)在 7.1秒中完成,但是test2(50个主题)已在 5.8秒中完成,而test1的上下文切换次数超过test2。 我在 Intel Core i5 Mac OS 10.7.5 上运行了相同的测试,并得到了相同的结果。

5 个答案:

答案 0 :(得分:45)

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

结果如你所描述的那样。 50线程版本略快。这是我的结果的方框图:

Parallel task comparison graph

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

当你使用50个线程时,每个线程都没有什么可做的。因此,单个线程中的任何延迟都会对总时间产生不太显着的影响。当调度程序忙于将内核配置为大量短线程时,可以通过在另一个内核上提供这些线程的时间来补偿一个内核的延迟。延迟对一个核心的总影响不是一个显示阻滞。

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


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

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

Additional test plot - low thread count

第二个图是针对更高的线程数(从核心数的3倍到60)。

Additional test plot - high thread count

在上面,随着线程数的增加,你可以看到明显的下降趋势。随着线程数的增加,您还可以看到结果的扩展范围变窄。

在这个测试中,有趣的是注意到4线程和50线程测试的性能大致相同,并且4核心测试中结果的扩展并不像我原来的测试那么宽。因为计算机没有做太多其他事情,所以可以将时间用于测试。重复测试同时将一个核心置于75%负载下会很有趣。

为了保持透视,请考虑一下:

Scaling threads


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

我决定只用四的倍数进行测试,并认为我可能同时找到收益递减点。所以我使用的线程数是2的幂,最高可达1024.我本来会更高,但Windows会在大约1400个线程中出错。

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

Trend for exponentiating the thread-count

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

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

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

答案 1 :(得分:3)

这完全取决于您的主题正在做什么

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

CPU结合

如果您的线程受CPU限制(意味着他们将大部分时间花在对内存中的数据进行计算),那么通过增加高于内核数量的线程数,您将看不到什么改进。实际上,在运行更多线程的情况下,失去效率,因为必须在线程上打开和关闭CPU内核的线程。

I / O约束

其中(#threads&gt; #cores)帮助,当你的线程受I / O约束时,意味着他们大部分时间都在等待I / O,(硬盘,网络,其他硬件等)在这种情况下,阻塞等待I / O完成的线程将从CPU中拔出,而实际准备做某事的线程将被放入。

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

答案 2 :(得分: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编译代码。它对发布的代码没有任何区别 - 但只是为了覆盖编译器优化代码的替代方案。

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

答案 3 :(得分:2)

我刚刚在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

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

答案 4 :(得分:0)

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

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

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

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


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