C 仅在MPI上使用OpenMp和MPI时没有加速

C 仅在MPI上使用OpenMp和MPI时没有加速,c,parallel-processing,mpi,openmp,distributed-computing,C,Parallel Processing,Mpi,Openmp,Distributed Computing,我已经阅读了我发现的所有相关问题,但仍然找不到解决问题的方法,因为我有一个带有双for循环的函数,这是我程序的瓶颈 代码是根据MPI设计的: 有一个很大的矩阵,我把它分散在p个进程中。 现在每个进程都有一个子矩阵。 每个进程都在循环中调用update。 当循环终止时,主进程收集结果。 现在我想用OpenMp扩展我的MPI代码,通过利用更新的双for循环来获得更快的执行 我在两台计算机上运行这个,每台计算机都有两个CPU。我使用4个进程。但是,无论是否使用OpenMp,我都看不到加速效果。有什么想

我已经阅读了我发现的所有相关问题,但仍然找不到解决问题的方法,因为我有一个带有双for循环的函数,这是我程序的瓶颈

代码是根据MPI设计的:

有一个很大的矩阵,我把它分散在p个进程中。 现在每个进程都有一个子矩阵。 每个进程都在循环中调用update。 当循环终止时,主进程收集结果。 现在我想用OpenMp扩展我的MPI代码,通过利用更新的双for循环来获得更快的执行


我在两台计算机上运行这个,每台计算机都有两个CPU。我使用4个进程。但是,无论是否使用OpenMp,我都看不到加速效果。有什么想法吗?我正在使用-O1优化标志进行编译。

这个未测试的版本怎么样

请编译并测试它。如果效果更好,我会详细解释。 顺便说一句,使用一些更激进的编译器选项可能也会有所帮助

void update (int pSqrt, int id, int subN, float** gridPtr, float ** gridNextPtr)
{
    int beg_i = 1, beg_j = 1;
    int end_i = subN - 1, end_j = subN - 1;
    if ( id / pSqrt == 0 ) {
        beg_i = 2;
    } else if ( id / pSqrt == (pSqrt - 1) ) {
        end_i = subN - 2;
    }
    if (id % pSqrt == 0) {
        beg_j = 2;
    } else if ((id + 1) % pSqrt == 0) {
        end_j = subN - 2;
    }
    #pragma omp parallel for schedule(static)
    for ( int i = beg_i; i < end_i; ++i ) {
        #pragma omp simd
        for ( int j = beg_j; j < end_j; ++j ) {
            gridNextPtr[i][j] = gridPtr[i][j] +
                parms.cx * (gridPtr[i+1][j] +
                        gridPtr[i-1][j] -
                        2.0 * gridPtr[i][j]) +
                parms.cy * (gridPtr[i][j+1] +
                        gridPtr[i][j-1] -
                        2.0 * gridPtr[i][j]);
        }
    }
}
编辑:我对代码所做的一些解释

最初的版本毫无理由地使用嵌套并行,一个并行区域嵌套在另一个并行区域中。这可能是非常反作用,我只是删除了它。 循环索引i和j在for循环语句之外声明和初始化。这在两个层面上是容易出错的:1/它可能会强制将其并行作用域声明为私有,而将它们放在for语句中会自动为它们提供正确的并行作用域;2/错误地重用循环外部的索引可能会造成混淆。将它们转移到for语句中很容易。 您在平行区域内更改j循环的边界是没有充分理由的。你必须宣布结束是私人的。此外,它是进一步开发的潜在限制,如collapse2指令的潜在使用,因为它打破了OpenMP标准中定义的规范循环形式的规则。因此,在平行区域之外定义一些beg_i和beg_j是有意义的,它节省了计算,简化了循环的形式,保持了它们的规范性。
从那以后,代码就适合矢量化了,如果编译器自己看不到可能的矢量化,那么在j循环上添加一个简单的simd指令将强制执行它。

这个未测试的版本呢

请编译并测试它。如果效果更好,我会详细解释。 顺便说一句,使用一些更激进的编译器选项可能也会有所帮助

void update (int pSqrt, int id, int subN, float** gridPtr, float ** gridNextPtr)
{
    int beg_i = 1, beg_j = 1;
    int end_i = subN - 1, end_j = subN - 1;
    if ( id / pSqrt == 0 ) {
        beg_i = 2;
    } else if ( id / pSqrt == (pSqrt - 1) ) {
        end_i = subN - 2;
    }
    if (id % pSqrt == 0) {
        beg_j = 2;
    } else if ((id + 1) % pSqrt == 0) {
        end_j = subN - 2;
    }
    #pragma omp parallel for schedule(static)
    for ( int i = beg_i; i < end_i; ++i ) {
        #pragma omp simd
        for ( int j = beg_j; j < end_j; ++j ) {
            gridNextPtr[i][j] = gridPtr[i][j] +
                parms.cx * (gridPtr[i+1][j] +
                        gridPtr[i-1][j] -
                        2.0 * gridPtr[i][j]) +
                parms.cy * (gridPtr[i][j+1] +
                        gridPtr[i][j-1] -
                        2.0 * gridPtr[i][j]);
        }
    }
}
编辑:我对代码所做的一些解释

最初的版本毫无理由地使用嵌套并行,一个并行区域嵌套在另一个并行区域中。这可能是非常反作用,我只是删除了它。 循环索引i和j在for循环语句之外声明和初始化。这在两个层面上是容易出错的:1/它可能会强制将其并行作用域声明为私有,而将它们放在for语句中会自动为它们提供正确的并行作用域;2/错误地重用循环外部的索引可能会造成混淆。将它们转移到for语句中很容易。 您在平行区域内更改j循环的边界是没有充分理由的。你必须宣布结束是私人的。此外,它是进一步开发的潜在限制,如collapse2指令的潜在使用,因为它打破了OpenMP标准中定义的规范循环形式的规则。因此,在平行区域之外定义一些beg_i和beg_j是有意义的,它节省了计算,简化了循环的形式,保持了它们的规范性。 从那以后,代码就适合矢量化了,如果编译器自己看不到可能的矢量化,那么在j循环上添加一个简单的simd指令将强制执行它。

高级分析 混合编程(如MPI+OpenMP)是一个好主意,这是一个常见的谬误。这种谬论得到了所谓的HPC专家的广泛支持,他们中的许多人都是超级计算中心的推纸工,不太会写代码。一位专家对MPI+线程谬论进行了剖析

这并不是说扁平MPI是最好的模型。例如,MPI专家支持在和中使用仅限两级MPI的方法。在所谓的MPI+MPI模型中,程序员使用MPI共享内存(而不是OpenMP)利用共享内存一致性域,但默认使用私有数据模型,这降低了竞争条件的发生率。此外,MPI+MPI只使用一个运行时系统,这使得资源管理和流程拓扑/关联更加容易。相比之下,MPI+OpenMP需要使用基本上不可扩展的fork-join执行模型和线程,即在OpenMP并行区域之间进行MPI调用,或者启用MPI_-THREAD_-MULTIPLE以便在线程区域内进行MPI调用—MPI_-THREAD_-MULTIPLE-entai 在今天的平台上,这是显而易见的开销

这个主题可能涉及很多页面,我现在没有时间写,所以请查看引用的链接

减少OpenMP运行时开销 MPI+OpenMP性能不如纯MPI的一个原因是OpenMP运行时开销往往出现在太多的地方。一种不必要的运行时开销来自嵌套并行。当一个嵌套在另一个内部的omp并行结构上时,就会发生嵌套并行。大多数程序员不知道并行区域是一个相对昂贵的构造,应该尽量减少它们。此外,omp parallel for是两种结构的融合——parallel和for——人们应该真正尝试独立思考这些结构。理想情况下,您可以创建一个包含许多工作共享构件(例如for、sections等)的平行区域

下面是您的代码修改为只使用一个并行区域,并跨两个for循环使用并行性。因为折叠不需要在两个for循环之间完美嵌套,所以我必须将一些代码移到内部。但是,在OpenMP降低后,没有任何东西可以阻止编译器将此循环不变量提升回原处。这是一个编译器概念,您可以忽略它,因此代码可能仍然只执行end_i次,而不是end_i*end_j次

更新:我修改了另一个答案中的代码来演示折叠

有多种方法可以使用OpenMP并行化这两个循环。下面您可以看到四个版本,它们都符合OpenMP 4。至少在当前的编译器中,版本1可能是最好的。版本2使用折叠,但不使用simd。它与Open3兼容。版本3可能是最好的,但在理想情况下更难实现,并且不会导致使用某些编译器生成SIMD代码。版本4只并行化外部循环

您应该尝试看看这些选项中哪一个对您的应用程序最快

对于以下编译器,上面的示例代码是正确的:

通用条款5.3.0 铿锵OpenMP 3.5.0 克雷C 8.4.2 英特尔16.0.1。 它不会使用PGI 11.7编译,因为[restrict]将其替换为[]就足够了,而且OpenMP simd子句。与此相反,此编译器缺少对C99的完全支持。鉴于OpenMP于2011年发布,它不符合OpenMP也就不足为奇了。遗憾的是,我无法访问较新版本。

高级分析 混合编程(如MPI+OpenMP)是一个好主意,这是一个常见的谬误。这种谬论得到了所谓的HPC专家的广泛支持,他们中的许多人都是超级计算中心的推纸工,不太会写代码。一位专家对MPI+线程谬论进行了剖析

这并不是说扁平MPI是最好的模型。例如,MPI专家支持在和中使用仅限两级MPI的方法。在所谓的MPI+MPI模型中,程序员使用MPI共享内存(而不是OpenMP)利用共享内存一致性域,但默认使用私有数据模型,这降低了竞争条件的发生率。此外,MPI+MPI只使用一个运行时系统,这使得资源管理和流程拓扑/关联更加容易。相比之下,MPI+OpenMP需要使用基本上不可扩展的fork-join执行模型和线程,即在OpenMP并行区域之间进行MPI调用,或者启用MPI_-THREAD_-MULTIPLE,以便在线程区域内进行MPI调用—MPI_-THREAD_-MULTIPLE在当今的平台上带来了显著的开销

这个主题可能涉及很多页面,我现在没有时间写,所以请查看引用的链接

减少OpenMP运行时开销 MPI+OpenMP性能不如纯MPI的一个原因是OpenMP运行时开销往往出现在太多的地方。一种不必要的运行时开销来自嵌套并行。当一个嵌套在另一个内部的omp并行结构上时,就会发生嵌套并行。大多数程序员不知道并行区域是一个相对昂贵的构造,应该尽量减少它们。此外,omp parallel for是两种结构的融合——parallel和for——人们应该真正尝试独立思考这些结构。理想情况下,您可以创建一个包含许多工作共享构件(例如for、sections等)的平行区域

下面是您的代码修改为只使用一个并行区域,并跨两个for循环使用并行性。因为折叠不需要在两个for循环之间完美嵌套,所以我必须将一些代码移到内部。但是,在OpenMP降低后,没有任何东西可以阻止编译器将此循环不变量提升回原处。这是一个编译器概念,您可以忽略它,因此代码可能仍然只执行end_i次,而不是end_i*end_j次

更新:我修改了另一个答案中的代码来演示折叠

有各种各样的wa ys使用OpenMP并行化这两个循环。下面您可以看到四个版本,它们都符合OpenMP 4。至少在当前的编译器中,版本1可能是最好的。版本2使用折叠,但不使用simd。它与Open3兼容。版本3可能是最好的,但在理想情况下更难实现,并且不会导致使用某些编译器生成SIMD代码。版本4只并行化外部循环

您应该尝试看看这些选项中哪一个对您的应用程序最快

对于以下编译器,上面的示例代码是正确的:

通用条款5.3.0 铿锵OpenMP 3.5.0 克雷C 8.4.2 英特尔16.0.1。

它不会使用PGI 11.7编译,因为[restrict]将其替换为[]就足够了,而且OpenMP simd子句。与此相反,此编译器缺少对C99的完全支持。鉴于OpenMP于2011年发布,它不符合OpenMP也就不足为奇了。不幸的是,我没有访问更新版本的权限。

@HighPerformanceMark不,没有。我在第一个for循环之前就有了它,但在研究了相关问题后,我决定把它移到那里。但是,我没有看到时间执行上的任何变化。好的,那么@HighPerformanceMark我可能对我读到的答案进行了错误的解释,所以我将pragma移到for循环上方。但是,不管有没有它,它仍然给我同样的时间。我更新了我的问题@HighPerformanceMark。现在好点了吗?我不知道你说的是什么“高性能标记”。放弃所有与MPI有关的内容?但我想说的是,MPI+OpenMp并不像我预期的那样比MPI版本快?对于少量内核,混合并行程序比纯MPI快是很正常的。对于纯MPI已经存在问题的大量节点,您应该在缩放比例上搜索差异。@HighPerformanceMark否否。我在第一个for循环之前就有了它,但在研究了相关问题后,我决定把它移到那里。但是,我没有看到时间执行上的任何变化。好的,那么@HighPerformanceMark我可能对我读到的答案进行了错误的解释,所以我将pragma移到for循环上方。但是,不管有没有它,它仍然给我同样的时间。我更新了我的问题@HighPerformanceMark。现在好点了吗?我不知道你说的是什么“高性能标记”。放弃所有与MPI有关的内容?但我想说的是,MPI+OpenMp并不像我预期的那样比MPI版本快?对于少量内核,混合并行程序比纯MPI快是很正常的。对于纯MPI已经存在问题的大量节点,您应该搜索在缩放方面的差异。谢谢您的回答。我像这样编译:mpicc-O1-omycheat-myheat.c-lm您使用什么编译器?我使用了-std=c99标志,现在就可以了。我再也看不到加速了!您是否尝试了错误的schedulestatic,1?如果是这样,请不要尝试。试一下-O3-mtune=native-march=native假设你用的是GCCThanks-Gilles,你能解释一下吗?谢谢你的回答。我像这样编译:mpicc-O1-omycheat-myheat.c-lm您使用什么编译器?我使用了-std=c99标志,现在就可以了。我再也看不到加速了!您是否尝试了错误的schedulestatic,1?如果是这样,请不要尝试。试试-O3-mtune=native-march=native假设你用的是GCCThanks-Gilles,你能解释一下吗?谢谢你的回答,杰夫。然而,我用2个进程执行这段代码,它似乎没有结束。有什么想法吗?可能代码需要privatei,j,end_i,end_j或类似的东西。不提供的陷阱之一是第三方无法验证它或对其进行任何修改。我知道Jeff,但代码很大。我在第二个的旁边放了一个printf,我得到了I=1,j=3,end_I=2049,end_j=2049i=1,j=3,end_I=2049,end_j=2049,我似乎得到了[1,3]中的值并进行了循环。我应该在哪里安排私人房间?就在collapse2旁边?是的。因为它的价值是卓越的。这是我对OpenMP语法的默认参考。请随意将代码放在Github上,而不是尝试在此处内联。谢谢您的精彩回答。然而,我用2个进程执行这段代码,它似乎没有结束。有什么想法吗?可能代码需要privatei,j,end_i,end_j或类似的东西。不提供的陷阱之一是第三方无法验证它或对其进行任何修改。我知道Jeff,但代码很大。我在第二个的旁边放了一个printf,我得到了I=1,j=3,end_I=2049,end_j=2049i=1,j=3,end_I=2049,end_j=2049,我似乎得到了[1,3]中的值并进行了循环。我应该在哪里安排私人房间?就在collapse2旁边?是的。因为它的价值是卓越的。这是我对OpenMP语法的默认参考。请随意将代码放在Github上,而不是尝试在此处内联。
#if VERSION==1
#define OUTER _Pragma("omp parallel for")
#define INNER _Pragma("omp simd")
#elif VERSION==2
#define OUTER _Pragma("omp parallel for collapse(2)")
#define INNER
#elif VERSION==3
#define OUTER _Pragma("omp parallel for simd collapse(2)")
#define INNER
#elif VERSION==4
#define OUTER _Pragma("omp parallel for simd")
#define INNER
#else
#error Define VERSION
#define OUTER
#define INNER
#endif


struct {
    float cx;
    float cy;
} parms;

void update (int pSqrt, int id, int subN, const float * restrict gridPtr[restrict], float * restrict gridNextPtr[restrict])
{
    int beg_i = 1, beg_j = 1;
    int end_i = subN - 1, end_j = subN - 1;
    if ( id / pSqrt == 0 ) {
        beg_i = 2;
    } else if ( id / pSqrt == (pSqrt - 1) ) {
        end_i = subN - 2;
    }
    if (id % pSqrt == 0) {
        beg_j = 2;
    } else if ((id + 1) % pSqrt == 0) {
        end_j = subN - 2;
    }
    OUTER
    for ( int i = beg_i; i < end_i; ++i ) {
        INNER
        for ( int j = beg_j; j < end_j; ++j ) {
            gridNextPtr[i][j] = gridPtr[i][j] + parms.cx * (gridPtr[i+1][j] + gridPtr[i-1][j] - 2 * gridPtr[i][j])
                                              + parms.cy * (gridPtr[i][j+1] + gridPtr[i][j-1] - 2 * gridPtr[i][j]);
        }
    }
}