我试图优化和并行化一些代码,这些代码在带有链接和节点的图形结构中进行模拟。一个主要的热点是像这样的循环:
void ExecuteAll()
{
for(long i = TotalCount(); i >= 1; i--)
{
long k = linkOrder[i];
long link = firstLink[k];
if (link == 0)
continue;
double d = 0.;
for(; link != 0; link = nextLink[link])
{
long kk = getNode(link);
d += fak[link]*res[kk];
}
d += res[k];
double d2 = d/fak2[k];
res[k] = d2;
res2[k] += d2;
}
}
我通过实现这样的类来重写这个以使用多个线程:
class myDemoClass
{
bool volatile *isDone;
public:
void ExecuteSlice()
{
for(long i = TotalCount() - mThreadIndex; i >= 1; i -= threadCount)
{
long k = linkOrder[i];
Execute(k);
}
}
void Execute(long k)
{
long link = firstLink[k];
if (link == 0)
{
isDone[k] = true;
return;
}
double d = 0.;
for(; link != 0; link = nextLink[link])
{
long kk = getNode(link);
for(int x = 0; ! isDone[kk]; x++)
{} // Wait until data is ready. Time too short for sleep or event
d += fak[link]*res[kk];
}
d += res[k];
double d2 = d/fak2[k];
res[k] = d2;
res2[k] += d2;
isDone[k] = true;
}
}
我为每个线程创建了这个类的实例,其中每个线程对i
的所有值的一片进行操作。我引入了一个新数组bool volatile *isDone
,因为我不能使用未处理节点的结果。我尝试插入一个睡眠或事件而不是for(;...;){}
循环,但事实证明等待状态太短了。
这似乎工作正常。所有对Execute()的调用只有10%必须进入等待循环,因为图形从起点开始越来越多,结果是正确的。
但令人惊讶的是,当使用8个线程的新代码时,8核XEON机器上没有可测量的性能增益(或损失)。我知道这个代码在缓存失效方面并不是最优的,主要是isDone
res
从多个内核写入和读取{{1}}。但在大多数情况下,取消引用的索引相距很远。该图表有大约1.000.000个节点和链接。
如何改善此代码的并行性?
答案 0 :(得分:5)
你不能像这样使用volatile
来使代码线程安全。 Volatile帮助单线程应用程序通过确保始终重新读取值来将变量映射到外部设备,但是对于线程来说它不够,因为编译器不仅可以重新排序语句,而且处理器可以重新排序指令。您应该为此使用适当的多线程原语,或者由库提供,或者是编译器特定的实现(例如,Win32上的互锁函数,或者gcc上的Atomic Builtins)。同样地,不清楚任何其他数据结构对于多线程修改是安全的。
至于性能,很难说出问题是什么,因为我们对你的图形结构一无所知,而且你的代码太抽象而无法解决它。但是,您似乎花了很多时间来迭代可能已经处理过的链接。理想情况下,您可以反过来这样做,然后从处理没有依赖关系的链接开始,然后在完成时,从依赖于此链接的链接开始,依此类推,这意味着没有等待。也许类似拓扑的东西在这里会有所帮助。
答案 1 :(得分:0)
OpenMP是事实上的标准,在当前编译器中得到很好的支持,用于将阵列处理代码并行化为结构相同的任务,您只希望在多个内核上运行而不必担心很多关于线程管理。看一下,它可能有助于澄清你的想法,甚至可以解决问题。
这最适合用于纯粹的CPU绑定任务 - 我不清楚你是否因为你提到等待数据到达而是这种情况?在这种情况下,多线程逻辑可能没有你期望或希望的那么多。
答案 2 :(得分:0)
一些可能有用的想法:
getNode()
用于多线程同步,如同其他提到的海报一样。它现在可能确实适合你,但不能保证它将来不会破坏。考虑一下最糟糕的情况,即从现在开始几个月后,它会略微破坏你的所有模拟结果但不足以显而易见。