并行求和数量是​​串行版本的两倍

时间:2016-10-05 16:09:51

标签: c++ c windows multithreading winapi

我的代码中有const LONGLONG UPPER = 1000000000;,我正在尝试计算从1到UPPER的所有数字之和(是的,我知道有一个公式)。

这些是我的全局:

const LONGLONG UPPER = 1000000000;
const int NUM = 10; // number of threads
LONGLONG g_sum;
CRITICAL_SECTION cs_sum;

这是我的线程功能:

DWORD WINAPI SumThread(PVOID pvParam) {
    LONGLONG i;
    LONGLONG sum = 0;
    LONGLONG x = (LONGLONG)pvParam;

    x = x * (UPPER / NUM);

    for (i = x + 1; i <= x + UPPER / NUM; i++) {
        sum += i;
    }

    EnterCriticalSection(&cs_sum);
    g_sum += sum;
    LeaveCriticalSection(&cs_sum);

    return 0;
}

这是我用来进行计算的代码:

HANDLE* hThreads = (HANDLE*)(malloc(sizeof(HANDLE) * NUM));
g_sum = 0;

InitializeCriticalSection(&cs_sum);
for (i = 0; i < NUM; i++) {
    hThreads[i] = CreateThread(NULL, 0, SumThread, (PVOID)i, 0, NULL);
}

WaitForMultipleObjects(NUM, hThreads, TRUE, INFINITE);
DeleteCriticalSection(&cs_sum);

但是我得到了奇怪的结果:当我在一个简单的(串行)for循环中对数字求和时,它的速度是多线程版本的两倍。当我将UPPER乘以10并将线程数增加到40时,多线程版本甚至不会停止(大约20分钟后)。这是什么原因?

3 个答案:

答案 0 :(得分:2)

有一些事情代表潜在的罪魁祸首。

首先(这通常是最重要的),检查您启用的编译器优化。在编译器优化方面,有两件事情经常是非常真实的:

  • 他们非常善于优化“累积循环”,这正是您在此代码中所做的。实际上,根据编译器的不同,他们可能会展开循环,或者使用SIMD操作来加快整个过程。
  • 他们不善于优化任何类型的多线程代码,无论代码有多简单。

在处理单线程与多线程累加器时,我发现了类似的结果,当优化关闭时,结果通常会反转(使多线程代码变得更快)。

作为一个案例研究,考虑编写的代码比“在xy之间添加所有数字”稍微简单一些,并查看多线程代码是否突然变得更有效。我的预测是它会,因为编译器将失去优化串行代码的方法。

其次,虽然这通常不代表大多数用例(可能不是你的)的大问题,但值得注意的是,启动新线程通常会涉及一定的开销。值得记住的。

最后一个建议是准确评估您正在执行计算的 。如果您编写了这样的代码:

size_t sum = 0;
std::mutex mutex;
std::thread t1([&]{for(size_t i = 0; i < 1'000'000; i++) {mutex.lock(); sum+=i; mutex.unlock();}});
std::thread t2([&]{for(size_t i = 1'000'000; i < 2'000'000; i++) {mutex.lock(); sum+=i; mutex.unlock();}});
t1.join();
t2.join();
std::cout << "Sum of integers between 0 and 1999999: " << sum << std::endl;

几乎肯定会比你 写的代码慢,这在功能上完全相同:

size_t sum = 0;
size_t s1 = 0, s2 = 0;
std::mutex mutex;
std::thread t1([&]{for(size_t i = 0; i < 1'000'000; i++) {s1 += i;} mutex.lock(); sum += s1; mutex.unlock();});
std::thread t2([&]{for(size_t i = 1'000'000; i < 2'000'000; i++) {s2 += i;}mutex.lock(); sum += s2; mutex.unlock();});
t1.join();
t2.join();
std::cout << "Sum of integers between 0 and 1999999: " << sum << std::endl;

可能(强调单词“可能”)如果你这样写的话可以获得一个小的加速(因为互斥锁/关键部分通常是主要的性能瓶颈):< / p>

size_t sum = 0;
size_t s1 = 0, s2 = 0;
std::thread t1([&]{for(size_t i = 0; i < 1'000'000; i++) {s1 += i;}});
std::thread t2([&]{for(size_t i = 1'000'000; i < 2'000'000; i++) {s2 += i;}});
t1.join();
t2.join();
sum = s1 + s2;
std::cout << "Sum of integers between 0 and 1999999: " << sum << std::endl;

当然,在这种情况下,这不是一个大问题,但总是值得考虑并牢记。

答案 1 :(得分:0)

绕线程费用很高。

我敢打赌,在这种情况下,对运行时间的最大影响是编译器优化和分支预测。在这个用例中,两者在串行版本中都会明显更好。

答案 2 :(得分:0)

你有多个线程访问一个共享的内存,并在其周围锁定。所有锁定,解锁,上下文切换,缓存命中等都会及时累积。单线程中的串行循环并不必担心。

我最近刚观看了一段视频(我将尝试找到它并在此处发布)解释了与您类似的设置,并展示了如何为每个线程提供自己专用的内存来进行操作,然后累积计算结果线程完成运行后的值可以在性能方面提供一些重大改进。

尝试更像这样的事情:

const LONGLONG UPPER = 1000000000;
const int NUM = 10; // number of threads

struct threadInfo
{
    LONGLONG start;
    LONGLONG sum;
};

DWORD WINAPI SumThread(PVOID pvParam) {
    struct threadInfo* pInfo = (struct threadInfo*) pvParam;
    LONGLONG i, sum = 0, x = pInfo->start;

    x *= (UPPER / NUM);

    for (i = x + 1; i <= x + UPPER / NUM; ++i) {
        sum += i;
    }

    pInfo->sum = sum;
    return 0;
}

struct threadInfo* pInfo = (struct threadInfo*) malloc (sizeof(struct threadInfo) * NUM);

HANDLE* hThreads = (HANDLE*) malloc(sizeof(HANDLE) * NUM);

for (int i = 0; i < NUM; ++i) {
    pInfo[i].start = i;
    hThreads[i] = CreateThread(NULL, 0, SumThread, &pInfo[i], 0, NULL);
}

WaitForMultipleObjects(NUM, hThreads, TRUE, INFINITE);

LONGLONG sum = 0;
for (int i = 0; i < NUM; ++i) {
    sum += pInfo[i].sum;
    CloseHandle(hThreads[i]);
}

free(pInfo);
free(hThreads);