我已经开始学习如何将OpenMP用作大学课程的一部分。作为实验练习,我们获得了一个需要并行化的系列程序。
我们首先了解False Sharing的危险之一,特别是在为循环并行更新数组时。
但是,我发现很难将以下代码片段转换为可并行化的任务,而不会导致错误共享:
int ii,kk;
double *uk = malloc(sizeof(double) * NX);
double *ukp1 = malloc(sizeof(double) * NX);
double *temp;
double dx = 1.0/(double)NX;
double dt = 0.5*dx*dx;
// Initialise both arrays with values
init(uk, ukp1);
for(kk=0; kk<NSTEPS; kk++) {
for(ii=1; ii<NX-1; ii++) {
ukp1[ii] = uk[ii] + (dt/(dx*dx))*(uk[ii+1]-2*uk[ii]+uk[ii-1]);
}
temp = ukp1;
ukp1 = uk;
uk = temp;
printValues(uk,kk);
}
我的第一反应是尝试分享 ukp1 :
for(kk=0; kk<NSTEPS; kk++) {
#pragma omp parallel for shared(ukp1)
for(ii=1; ii<NX-1; ii++) {
ukp1[ii] = uk[ii] + (dt/(dx*dx))*(uk[ii+1]-2*uk[ii]+uk[ii-1]);
}
temp = ukp1;
ukp1 = uk;
uk = temp;
printValues(uk,kk);
}
但与串行版相比,这显然显示出明显的减速。显而易见的原因是在对 ukp1 的一些写操作期间发生了虚假共享。
我的印象是,我可以使用 reduction 子句,但我很快发现这不能用于数组。
有什么东西可以用来并行化这段代码来改善运行时间吗?是否有我可以使用的条款,我没有听说过?或者这是我需要重构代码以实现正确的并行化的任务?
非常感谢所有形式的投入!
编辑:有人向我指出我的代码中存在错误。我本地的代码是正确的,我只是错误地编辑它(这改变了代码的结构),抱歉混淆!
EDIT2 :
@Sergey向我指出了一些我觉得有用的信息:
将uk或ukp1设置为private将基本上与将它们设置为共享具有相同的效果,因为它们指向同一内存位置的指针
使用静态调度应该在理论上帮助 ,但我仍然遇到同样的减速。另外,我觉得静态调度不是解决这个问题的最便携方式。
答案 0 :(得分:12)
因为我们首先讨论的是优化问题:
将常量定义为宏,允许编译器进行更好的优化。
#define dx (1.0/(double)NX)
#define dt (0.5*dx*dx)
使用OpenMP时,变量的默认共享规则为shared
,但最好将其设置为none
并手动启用并行部分内所需的每个变量。通过这种方式,您可以确保避免冲突。
#pragma omp parallel for default(none) shared(ukp1, uk)
同样将ukp1
或uk
设置为任何共享状态只会将指针传递到并行部分,因为您将它们声明为指针。所以它们中的记忆仍然是共享的。
最后,为了避免flase共享,您需要确保尽可能少地在线程之间共享缓存行。只读缓存行(因此在您的情况下为uk
)是无关紧要的,它们可以存在于每个线程中,但每个线程应写入缓存行ukp1
。今天缓存行通常是64字节长 - 因此一个缓存行将适合8 double
s。所以你想为每个线程分配至少8次迭代的块:
#pragma omp parallel for default(none) shared(ukp1, uk) schedule(static,8)
将每个块部署代码8次迭代,并且应该出现在内部循环中。
for(kk=0; kk<NSTEPS; kk++) {
#pragma omp parallel for default(none) shared(ukp1, uk) schedule(static,8)
for(ii=1; ii<NX-1; ii++) {
ukp1[ii] = uk[ii] + (dt/(dx*dx))*(uk[ii+1]-2*uk[ii]+uk[ii-1]);
}
// Swap pointers for the next time step
temp = ukp1;
ukp1 = uk;
uk = temp;
}
实际上,根据您的数据大小,您可能希望分配更大的块大小。我倾向于使用0x1000
- 在大多数系统上它甚至可以适合整个页面(假设你是页面对齐的)。
编辑:
为了让它真正产生效果,你需要正确对齐你的记忆。您从索引1
开始,所以:
double *uk = memalign(0x40 , sizeof(double) * (NX + 8));
double *ukp1 = memalign(0x40 , sizeof(double) * (NX + 8));
uk += 7;
ukp1 += 7;
现在ukp1[1]
是缓存行对齐的。增加块大小可能有所帮助,但除非您计划使用NX > 100000
,否则首先并行处理并不重要。
您需要记住,在每次迭代中重新启动并行部分会产生大量开销。为了控制这一点,您需要在简单的OpenMP之外重新设计您的日程安排。
答案 1 :(得分:4)
我相信@SergeyL。是的,你的代码应该更像这样:
// Parabolic 1D heat diffusion solved with an explicit method
for(kk=0; kk<NSTEPS; kk++) {
for(ii=1; ii<NX-1; ii++) {
ukp1[ii] = uk[ii] + (dt/(dx*dx))*(uk[ii+1]-2*uk[ii]+uk[ii-1]);
}
// Swap pointers for the next time step
temp = ukp1;
ukp1 = uk;
uk = temp;
}
这就是为了避免错误共享,您必须确保不同的线程不在同一缓存行上运行。这确实需要您选择适当的调度和块大小。想到的最简单的解决方案是:
// Parabolic 1D heat diffusion solved with an explicit method
#pragma omp parallel private(kk)
{
for(kk=0; kk<NSTEPS; kk++) {
#pragma omp for schedule(static)
for(ii=1; ii<NX-1; ii++) {
ukp1[ii] = uk[ii] + (dt/(dx*dx))*(uk[ii+1]-2*uk[ii]+uk[ii-1]);
}
#pragma omp single
{
// Swap pointers for the next time step
temp = ukp1;
ukp1 = uk;
uk = temp;
}
} // outer for loop
} // pragma omp parallel