多线程函数性能比单线程差

Posted

技术标签:

【中文标题】多线程函数性能比单线程差【英文标题】:Multithreaded function performance worse than single threaded 【发布时间】:2019-02-27 21:21:32 【问题描述】:

我写了一个在单线程上运行的update() 函数,然后我写了下面的函数updateMP(),它做同样的事情,除了我在一些线程之间划分我的两个for循环中的工作:

void GameOfLife::updateMP()

    std::vector<Cell> toDie;
    std::vector<Cell> toLive;

#pragma omp parallel
    
        // private, per-thread variables
        std::vector<Cell> myToDie;
        std::vector<Cell> myToLive;

#pragma omp for
        for (int i = 0; i < aliveCells.size(); i++) 
            auto it = aliveCells.begin();
            std::advance(it, i);

            int liveCount = aliveCellNeighbors[*it];
            if (liveCount < 2 || liveCount > 3) 
                myToDie.push_back(*it);
            
        

#pragma omp for
        for (int i = 0; i < aliveCellNeighbors.size(); i++) 
            auto it = aliveCellNeighbors.begin();
            std::advance(it, i);

            if (aliveCells.find(it->first) != aliveCells.end()) // is this cell alive?
                continue; // if so skip because we already updated aliveCells

            if (aliveCellNeighbors[it->first] == 3) 
                myToLive.push_back(it->first);
            
        

#pragma omp critical
        
            toDie.insert(toDie.end(), myToDie.begin(), myToDie.end());
            toLive.insert(toLive.end(), myToLive.begin(), myToLive.end());
        
    

    for (const Cell& deadCell : toDie) 
        setDead(deadCell);
    

    for (const Cell& liveCell : toLive) 
        setAlive(liveCell);
    

我注意到它的性能比单线程 update() 更差,而且似乎随着时间的推移越来越慢。

我想我可能因为两次使用omp for 而做错了什么?我是 OpenMP 新手,所以我还在研究如何使用它。

为什么我的多线程实现性能越来越差?

编辑:完整来源:https://github.com/k-vekos/GameOfLife/tree/hashing?files=1

【问题讨论】:

这通常是因为算法并行化不顺畅。多个线程争夺公共资源,浪费的时间多于节省的时间。另一个常见的一个是线程的开销超过了算法的运行时间。 @WilliamMiller 是的,我使用的是 Release 构建配置,这里是命令行:/GS /GL /analyze- /W3 /Gy /Zc:wchar_t /I"C:\SFML-2.5.1\include" /Zi /Gm- /O2 /sdl /Fd"Release\vc141.pdb" /Zc:inline /fp:precise /D "SFML_STATIC" /D "_MBCS" /errorReport:prompt /WX- /Zc:forScope /Gd /Oy- /Oi /MD /openmp /FC /Fa"Release\" /EHsc /nologo /Fo"Release\" /Fp"Release\GameOfLife.pch" /diagnostics:classic 我是用肉眼计时的,区别很明显。 这部分操作运行一次迭代需要多长时间?线程的创建完全有可能支配运行时,因为提到了@user4581301 我不知道 openmp,但这在我看来不像 minimal reproducible example。 康威人生游戏的每一步都很快,因此您可能花在同步上的时间比执行代码要多。 【参考方案1】:

为什么我的多线程实现性能越来越差?

经典问题:)

你只循环通过活着的细胞。这实际上很有趣。 Conway 的生命游戏的简单实现会查看每个单元格。你的版本优化了比死细胞更少的活细胞,我认为这在游戏后期很常见。我无法从您的摘录中看出,但我认为当活细胞与死细胞的比率较高时,它可能会通过做多余的工作来进行权衡。

omp parallel 的一个警告是,不能保证在并行部分的进入/退出期间不会创建/销毁线程。它依赖于实现。我似乎找不到有关 MSVC 实施的任何信息。有知道的请指点一下。

这意味着您的线程可能会在每个更新循环中被创建/销毁,这是一个沉重的开销。要做到这一点,工作量应该比开销贵几个数量级。

您可以分析/测量代码以确定开销和工作时间。它还应该可以帮助您了解真正的瓶颈在哪里。

Visual Studio 有一个带有漂亮 GUI 的分析器。您需要使用发布优化编译代码,但还需要包含调试符号(默认发布配置中不包括这些符号)。我还没有研究如何手动设置它,因为我通常使用 CMake,它会自动设置 RelWithDebInfo 配置。

使用high_resolution_clock 对难以使用分析器测量的部分进行计时。

如果你可以使用 C++17,它有一个标准的并行 for_each(ExecutionPolicy 重载)。大多数算法标准函数都可以。 https://en.cppreference.com/w/cpp/algorithm/for_each。它们太新了,以至于我对它们几乎一无所知(它们可能也有与 OpenMP 相同的问题)。

似乎随着时间的推移越来越慢。

可能是你没有清理你的向量之一吗?

【讨论】:

具体实现的推测是否正确,这才是真正的答案。即使线程被重用,生命的游戏是如此简单,以至于在线程之间切换可能比检查单个单元格花费更多的时间。【参考方案2】:

首先,如果你想要任何一种性能,你必须在关键部分做尽可能少的工作。我将从更改以下内容开始:

std::vector<Cell> toDie;
std::vector<Cell> toLive;

std::vector<std::vector<Cell>> toDie;
std::vector<std::vector<Cell>> toLive;

然后,在你的关键部分,你可以这样做:

toDie.push_back(std::move(myToDie));
toLive.push_back(std::move(myToLive));

可以说,向量的向量并不可爱,但这将防止在 CS 内部进行深度复制,这是不必要的时间消耗。

[更新] 恕我直言,如果您使用非连续数据结构,则使用多线程毫无意义,至少不是那样。事实上,您将花费大部分时间等待缓存未命中,因为这是关联容器所做的事情,而很少做实际工作。 我不知道这游戏怎么玩。感觉就像我必须做大量更新和渲染的事情,我会在“主”线程上尽快完成更新,并且我会为渲染器设置另一个(分离的)线程。然后,您可以在每次“更新”后为渲染器提供结果,并在渲染时执行另一次更新。

另外,我绝对不是散列方面的专家,但hash&lt;int&gt;()(k.x * 3 + k.y * 5) 似乎是高冲突散列。你当然可以尝试其他类似here建议的方法

【讨论】:

"...如果在循环内定义迭代器,则不是最佳选择" 是否可以使用带有索引的 for 循环迭代 unordered_map?我以为我只能通过传入模板类型的对象来使用[] 运算符。另外,抱歉,在我的 OP 中添加了指向完整源代码的链接。

以上是关于多线程函数性能比单线程差的主要内容,如果未能解决你的问题,请参考以下文章

多线程比单线程慢

多线程 - 比单线程慢

由于 CPU 类型,C++ Boost 多线程比单线程慢?

多线程并发一定比单线程快吗?

python多线程不能利用多核cpu,但有时候多线程确实比单线程快。

java - 多线程中的简单计算比单线程中需要更长的时间