我编写了一个在单个线程上运行的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
答案 0 :(得分:2)
为什么我的多线程实现的性能会变差?
经典问题:)
您只能遍历活着的细胞。这实际上很有趣。康威《生命游戏》的幼稚实现会观察每个单元。您的版本优化后的存活细胞数量少于死细胞,我认为这在游戏后期很常见。我不能从您的摘录中看出来,但是我认为当活细胞与死细胞的比率更高时,它可能会做一些多余的工作来权衡。
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一样有问题)。
似乎随着时间的流逝越来越慢。
可能是您没有清除其中一个媒介吗?
答案 1 :(得分:0)
首先,如果您想要任何性能,则必须在关键部分中尽可能少地进行工作。我将从更改以下内容开始:
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