我正在编写一个执行一些长计算的程序,我可以根据需要分成任意数量的任务。为了便于讨论,让我们假设我正在编写一种算法,通过尝试将它除以2和p-1之间的所有数来找出数p是否为素数。显然,这个任务可以分解为许多线程。
我实际上写了一个样本应用程序就是这么做的。作为一个参数,我给出了我想要检查的数字,以及要使用的线程数(每个线程都有一个相同大小的数字范围来尝试除以p - 它们一起覆盖整个范围)。
我的机器有8个核心。我开始使用大量的程序运行程序,我知道它是素数(2971215073),并且有1,2,3个线程等,直到达到8个线程 - 每次程序运行速度比前一个快,这是我的预期。但是,当我尝试大于8的数字时,计算时间实际上变得越来越小(即使是一点点)!
我的线程中没有I / O或类似内容,只有纯cpu计算。当我通过8个线程时,我预计运行时间会变得更糟,因为会有更多的上下文切换,并行运行线程的数量保持在8个。很难说峰值在哪里因为差异很小而且变化很大从一次运行到另一次运行,但很明显,即50个线程以某种方式运行速度超过8(约300毫秒)......
我的猜测是因为我有这么多线程,所以我得到更多的运行时间,因为我在系统的线程池中有更大的部分,所以我的线程被选中更多。但是,我创建的线程越多,程序运行得越快就越有意义(否则为什么不是每个人都创建1000个线程?)。
任何人都可以提供一个解释,也许最好的做法是创建相对于机器核心数量的线程数量?
感谢。
我感兴趣的人的代码(在Windows上编译,VS2012):
#include <Windows.h>
#include <conio.h>
#include <iostream>
#include <thread>
#include <vector>
using namespace std;
typedef struct
{
unsigned int primeCandidate;
unsigned int rangeStart;
unsigned int rangeEnd;
} param_t;
DWORD WINAPI isDivisible(LPVOID p)
{
param_t* param = reinterpret_cast<param_t*>(p);
for (unsigned int d = param->rangeStart; d < param->rangeEnd; ++d)
{
if (param->primeCandidate % d == 0)
{
cout << param->primeCandidate << " is divisible by " << d << endl;
return 1;
}
}
return 0;
}
bool isPrime(unsigned int primeCandidate, unsigned int numOfCores)
{
vector<HANDLE> handles(numOfCores);
vector<param_t> params(numOfCores);
for (unsigned int i = 0; i < numOfCores; ++i)
{
params[i].primeCandidate = primeCandidate;
params[i].rangeStart = (primeCandidate - 2) * (static_cast<double>(i) / numOfCores) + 2;
params[i].rangeEnd = (primeCandidate - 2) * (static_cast<double>(i+1) / numOfCores) + 2;
HANDLE h = CreateThread(nullptr, 0, reinterpret_cast<LPTHREAD_START_ROUTINE>(isDivisible), ¶ms[i], 0, 0);
if (NULL == h)
{
cout << "ERROR creating thread: " << GetLastError() << endl;
throw exception();
}
handles[i] = h;
}
DWORD ret = WaitForMultipleObjects(numOfCores, &handles[0], TRUE, INFINITE);
if (ret >= WAIT_OBJECT_0 && ret <= WAIT_OBJECT_0 + numOfCores - 1)
{
for (unsigned int i = 0; i < numOfCores; ++i)
{
DWORD exitCode = -1;
if (0 == GetExitCodeThread(handles[i], &exitCode))
{
cout << "Failed to get thread's exit code: " << GetLastError() << endl;
throw exception();
}
if (1 == exitCode)
{
return false;
}
}
return true;
}
else
{
cout << "ERROR waiting on threads: " << ret << endl;
throw exception();
}
}
int main()
{
unsigned int primeCandidate = 1;
unsigned int numOfCores = 1;
cout << "Enter prime candidate: ";
cin >> primeCandidate;
cout << "Enter # of cores (0 means all): ";
cin >> numOfCores;
while (primeCandidate > 0)
{
if (0 == numOfCores) numOfCores = thread::hardware_concurrency();
DWORD start = GetTickCount();
bool res = isPrime(primeCandidate, numOfCores);
DWORD end = GetTickCount();
cout << "Time: " << end-start << endl;
cout << primeCandidate << " is " << (res ? "" : "not ") << "prime!" << endl;
cout << "Enter prime candidate: ";
cin >> primeCandidate;
cout << "Enter # of cores (0 means all): ";
cin >> numOfCores;
}
return 0;
}
答案 0 :(得分:5)
是。以下是我在i7 / Vista 64盒子上进行的一些测试的一小部分摘录,(4'真'核+超线程):
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
..表明,与您的测试一样,线程的“超额预订”确实导致整体执行时间略微提高2-3%。我的测试将简单的'计算整数'CPU密集型任务提交给具有不同线程数的线程池。
我当时的结论是,小的改进是因为大量的线程占据了我的盒子上“基本负载”的较大%年龄--1000%的负载的1-4% - 几乎总是空闲的Firefox,uTorrent,Word,任务栏等中的奇怪线程在测试期间碰巧运行了一段时间。
在我的测试中,似乎使用64个线程而不是8个线程的“上下文切换开销”可以忽略不计,并且可以忽略。
这仅适用于任务使用的数据非常小的情况。我后来重复了一系列类似的测试,其中任务使用了8K阵列 - L1缓存的大小。在这种“最坏情况”的情况下,使用比核心更多的线程导致非常明显的减速,直到16个线程及以上,当线程交换整个缓存进出时,性能下降了40%。超过大约20个线程,减速没有变得更糟,因为无论有多少线程运行任务,缓存仍然以相同的速率从每个核心交换出来。
另请注意,我有足够的内存,因此页面错误很少。
答案 1 :(得分:1)
您假设每个线程都有相同数量的工作要执行,实际情况可能并非如此。您应该注意的是每个线程的退出时间。如果它们中的一个或多个比其余的更早退出,那么添加更多线程将加速它将是有意义的。也就是说,如果提前停止,则意味着将不再使用核心,通过额外的线程可以更公平地分解负载。
有几个原因可能导致每个线程执行不同的执行时间。我不知道代码的基本指令时序,但也许它们是可变的。也可能是每个线程都有一组不同的CPU优化,比如分支预测。人们可能只是失去了对操作系统的时间片,或者暂时停留在它的微小内存上。可以说,有许多因素可能使一个比另一个慢。
哪个是最好的计数很难说。通常,您希望保持CPU的加载,因此对于N个内核,N通常是正确的。但是,要注意像超线程这样的东西,你实际上并没有额外的内核 - 除非你有大量的内存使用,否则,超线程将只会妨碍你。在AMD的新芯片上,它们有一半的FPU,所以你的整数指令很好,但浮点可能会停滞。
如果您希望保持每个CPU的加载,那么实现它的唯一方法就是使用基于作业的框架。将您的计算分解为更小的单位(就像您一样),但每个核心仍然只有一个线程。当一个线程完成当前的工作时,它应该接受下一个可用的工作。这样,如果某些作业更长/更短,则释放的CPU将继续执行下一个作业。
这当然只有在计算时很长才有意义。如果总时间只有几秒钟,则作业的开销可能会导致轻微的减速。但即使从4-5秒开始你也应该开始看到收益。此外,确保在进行小型定时测试时关闭CPU频率调整,否则每个CPU的加速/减速时间基本上会为您提供随机结果。