首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >问答首页 >多线程从单线程循环中慢于预期。

多线程从单线程循环中慢于预期。
EN

Code Review用户
提问于 2019-11-03 11:09:11
回答 3查看 2.9K关注 0票数 3

下面的代码是我在C++中使用多线程的优点而运行的一个实验。给定一个数字10000000000,它计算出在110000000000的范围内,有多少个数字是偶数,可被5整除,可被8整除,可被10整除。

首先,它运行单线程函数,然后运行多线程函数.

然而,问题是,我得到的结果并不像预期的那样。这表明多线程根本没有任何好处。我甚至不使用互斥,而是使用多个线程。

使用的

编译器/ IDE:

MicrosoftVisualStudio2019社区版,C++17,x64发布版本构建与/O2优化。

代码:

代码语言:javascript
复制
#include <iostream>
#include <thread>
#include <chrono>
#include <string>
#include <vector>

#define CALC_NUMBER 10000000000ull

struct Counters {
    unsigned long long int CountDivTen = 0;
    unsigned long long int CountDivEight = 0;
    unsigned long long int CountDivFive = 0;
    unsigned long long int CountEven = 0;
};

Counters DivCounter;

// For multi-threading
std::vector<std::pair<unsigned long long, unsigned long long>> parts = {
    {1, 2500000000}, {2500000001, 5000000000}, {5000000001, 7500000000},    
    {7500000001, 10000000000}
};

// Multi-threading counters.
std::vector<Counters> MyCounters(4);


void SingleThreaded()
{
    std::chrono::high_resolution_clock::time_point StartTime = 
                         std::chrono::high_resolution_clock::now();

    for (unsigned long long x = 1; x <= CALC_NUMBER; ++x)
    {
        // Count the even number
        if ((x % 2) == 0)
            ++DivCounter.CountEven;

        // Count divisible by 5
        if ((x % 5) == 0)
            ++DivCounter.CountDivFive;

        // Count divisible by 8
        if ((x % 8) == 0)
            ++DivCounter.CountDivEight;

        // Count divisible by 10
        if ((x % 10) == 0)
            ++DivCounter.CountDivTen;
    }

    auto elapsed = std::chrono::high_resolution_clock::now() - StartTime;
    auto seconds = std::chrono::duration_cast<std::chrono::seconds>(elapsed).count();
    std::cout << "Time in seconds: " << seconds << std::endl;
}

void MultiThread_Merge(int index)
{
    for (unsigned long long x = parts[index].first; x <= parts[index].second; ++x)
    {
        // Count the even number
        if ((x % 2) == 0)
            ++MyCounters[index].CountEven;

        // Count divisible by 5
        if ((x % 5) == 0)
            ++MyCounters[index].CountDivFive;

        // Count divisible by 8
        if ((x % 8) == 0)
            ++MyCounters[index].CountDivEight;

        // Count divisible by 10
        if ((x % 10) == 0)
            ++MyCounters[index].CountDivTen;
    }
}

void DoThreadUsingMerge()
{
    // Start timer
    std::chrono::high_resolution_clock::time_point StartTime = 
                     std::chrono::high_resolution_clock::now();

    // Create four Threads
    std::vector<std::thread> MyThreads(4);

    // Create Threads
    for (size_t i = 0; i < MyThreads.size(); ++i) {

        MyThreads[i] = std::thread(MultiThread_Merge, i);
    }

    // Wait for all threads to finish.
    for (size_t i = 0; i < MyThreads.size(); ++i) {

        MyThreads[i].join();
    }

    // When threads are done, add up numbers.
    for (auto i : MyCounters)
    {
        // Add all the numbers.
        DivCounter.CountEven += i.CountEven;
        DivCounter.CountDivFive += i.CountDivFive;
        DivCounter.CountDivEight += i.CountDivEight;
        DivCounter.CountDivTen += i.CountDivTen;
    }

    // Stop timer and get time.
    auto elapsed = std::chrono::high_resolution_clock::now() - StartTime;
    auto seconds = std::chrono::duration_cast<std::chrono::seconds>(elapsed).count();
    std::cout << "Time in seconds: " << seconds << std::endl;
}

void DisplayCounters()
{
    std::cout << "Count divisible by 2: " << DivCounter.CountEven << std::endl;
    std::cout << "Count divisible by 5: " << DivCounter.CountDivFive << std::endl;
    std::cout << "Count divisible by 8: " << DivCounter.CountDivEight << std::endl;
    std::cout << "Count divisible by 10: " << DivCounter.CountDivTen << std::endl;
}


int main()
{
    std::cout << "Calculation number: " << CALC_NUMBER << std::endl;

    std::cout << "\n============================================\n";
    std::cout << "\nSingle-Threaded ..\n";

    SingleThreaded();

    DisplayCounters();

    // Reset for multi-thread
    DivCounter.CountEven = 0;
    DivCounter.CountDivFive = 0;
    DivCounter.CountDivEight = 0;
    DivCounter.CountDivTen = 0;

    std::cout << "\n============================================\n";
    std::cout << "\nMulti-Threaded (Merge) ..\n";

    DoThreadUsingMerge();
    DisplayCounters();

    system("pause");
}

输出:

下面是4个核心CPU的输出,Windows8.1操作系统:

计算编号: 10000000000 ============================================单线程.时间(秒):36 计数可被2: 50000000堆栈除以纽线计数可被5: 20000000堆栈除以纽线计数可除以8: 12500000堆栈纽线计数可除以10: 1000000000 ============================================多线程(合并)。时间(秒):42 计数可被2: 50000000堆栈整除,纽线计数可被5: 20000000堆栈除以纽线计数可除以8: 12500000堆栈纽线计数可被10: 10000000StackNewline计数除以10: 1000000000按任意键继续。。。

下面是6核心CPU ( Windows 10 OS)的输出:

计算编号: 10000000000 ============================================单线程.时间(秒):32 计数可被2: 50000000堆栈除以纽线计数可被5: 20000000堆栈除以纽线计数可除以8: 12500000堆栈纽线计数可除以10: 1000000000 ============================================多线程(合并)。时间(秒):45 计数可被2: 50000000堆栈除以纽线计数可被5: 20000000堆栈除以纽线计数可除以8: 12500000堆栈纽线计数可被10: 10000000StackNewline计数除以10: 1000000000按任意键继续。。。

结果:

结果表明,无论多少次,无论内核多少次,多线程代码都不会从线程中受益。

问题:

这段代码不像预期那样运行的原因是什么?我是不是偶然发现了一些代码,这些代码不能或不能从多线程中获益?

备注:

我还试图把计算次数增加10倍,20倍,30倍,等等。但我没有看到任何更好的表现。

EN

回答 3

Code Review用户

回答已采纳

发布于 2019-11-03 19:34:43

假共享

struct Counters比缓存行小。对于良好的只读数据结构,数据的“干净”副本可以在缓存中处于共享状态,因此如果两个或多个内核想要拥有相同的数据,那就无关紧要了。但是在这里,它被用于许多读/写/修改操作,多个核试图跳到同一条高速缓存线上--而不是完全相同的数据,所以这不是真正的共享,数据在逻辑上是独立的,但从共享硬件的角度来看,由于它位于同一高速缓存线上:错误共享。

Counters填充到64个字节可以工作。在某种程度上。至少它将开始使用线程计数进行适当的扩展,但是代码仍然足够慢,在它超过单线程版本之前,我需要4个线程。

偶然悲观的内环

从编译器的角度来看,存在对共享内存的写入(和从共享内存中读取)。也许它们是必要的,它怎么知道它们不是呢?但是我们人类,用我们的全程序推理技巧,知道其中的大部分是不必要的,因为主线程等待工人完成,然后收集结果,部分计数在过程中不被观察,所以我们可以这样做:

代码语言:javascript
复制
void MultiThread_Merge(int index)
{
    Counters local;
    for (unsigned long long x = parts[index].first; x <= parts[index].second; ++x)
    {
        // Count the even number
        if ((x % 2) == 0)
            ++local.CountEven;

        // Count divisible by 5
        if ((x % 5) == 0)
            ++local.CountDivFive;

        // Count divisible by 8
        if ((x % 8) == 0)
            ++local.CountDivEight;

        // Count divisible by 10
        if ((x % 10) == 0)
            ++local.CountDivTen;
    }

    MyCounters[index] = local;
}

现在它很快了。

票数 4
EN

Code Review用户

发布于 2019-11-03 16:54:53

我正在写一个单独的答案,因为我已经测试了它。

我编译了您的代码并运行了相同的测试,结果相同。经过一些尝试之后,问题似乎是四个线程都访问相同的结构计数向量(std::vector<Counters> MyCounters(4);)。

我用一个简单变量(unsigned long long int My1Counter;)替换了计数器,对于多线程运行,我现在得到了3.8分的改进(这仍然在dev环境中,剩下的.2可能是dev env吃了一点)。

我的猜测是向量类是‘线程安全’,因此每次从其中一个线程访问它时都会锁定它,因此其他三个线程必须等待。

您可以尝试一个简单的C-数组来验证它,因为它并不是隐式线程安全的。

票数 1
EN

Code Review用户

发布于 2019-11-03 15:26:52

你肯定会看到一个巨大的进步,几乎是N的一倍。

提到的开销在微秒范围内,每线程一次。

我有一个类似的例子,运行时下降了2,4,8的因素,这取决于我启动的线程数量(当然,直到我拥有的内核数量)。

我无法确切地告诉您为什么您的示例不起作用;也许编译器足够聪明,一开始可以在多个线程中运行,或者您的可执行文件无法访问来运行多个线程。你是不是在虚拟机里?检查int N = std::thread::hardware_concurrency(),看看你的程序也有多少线程可以访问。

我认为你的代码是正确的,应该有效。

票数 0
EN
页面原文内容由Code Review提供。腾讯云小微IT领域专用引擎提供翻译支持
原文链接:

https://codereview.stackexchange.com/questions/231773

复制
相关文章

相似问题

领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档