多线程函数性能比单线程差
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<int>()(k.x * 3 + k.y * 5)
似乎是高冲突散列。你当然可以尝试其他类似here建议的方法
【讨论】:
"...如果在循环内定义迭代器,则不是最佳选择" 是否可以使用带有索引的 for 循环迭代unordered_map
?我以为我只能通过传入模板类型的对象来使用[]
运算符。另外,抱歉,在我的 OP 中添加了指向完整源代码的链接。以上是关于多线程函数性能比单线程差的主要内容,如果未能解决你的问题,请参考以下文章