7

我正在编写一个执行一些长时间计算的程序,我可以将其分解为任意数量的任务。为了便于讨论,假设我正在编写一个算法,通过尝试将它除以 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), &params[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;
}
4

2 回答 2

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 个线程,速度并没有变得更糟,因为无论有多少线程运行任务,缓存仍然以相同的速率从每个内核中换出。

另请注意,我有足够的 RAM,因此页面错误很少。

于 2013-06-15T13:27:52.037 回答
1

您假设每个线程都有相同数量的工作要执行,但实际上可能并非如此。您应该查看的是每个线程的退出时间。如果它们中的一个或多个比其他线程明显更早退出,那么添加更多线程会加快速度是有道理的。也就是说,如果提早停止,则意味着将不再使用内核,通过拥有额外的线程,它可以更公平地分解负载。

每个线程可能需要不同的执行时间有几个原因。我不知道您的代码的基本指令时间,但也许它们是可变的。也可能是每个线程都有一组不同的 CPU 优化,比如分支预测。一个人可能只是失去了对操作系统的时间片,或者在它的少量内存上暂时停滞不前。可以说有许多因素可以使一个比另一个慢。

哪个是最好的计数很难说。通常,您希望保持 CPU 处于负载状态,因此对于 N 个内核的 N 个线程通常是正确的。但是,请注意诸如超线程之类的事情,您实际上并没有额外的内核——除非您使用大量内存,而您没有,否则超线程只会妨碍您。在 AMD 的较新芯片上,它们的 FPU 数量只有一半,因此您的整数指令很好,但浮点可能会停止。

如果您希望保持每个 CPU 的负载,那么真正做到这一点的唯一方法是使用基于作业的框架。将您的计算分解为更小的单元(就像您所做的那样),但每个核心仍然只有一个线程。当一个线程完成其当前工作时,它应该接受下一个可用工作。这样,如果某些作业更长/更短都没关系,释放的 CPU 将继续进行下一个作业。

这当然只有在计算很长时才有意义。如果总时间只有几秒钟,则作业的开销可能会导致轻微的减速。但即使从 4-5 秒开始,您也应该开始看到收益。此外,确保在进行小型时序测试时关闭 CPU 频率缩放,否则每个 CPU 的加速/减速时间基本上会给你随机结果。

于 2013-06-15T14:45:45.530 回答