65.9K
CodeProject 正在变化。 阅读更多。
Home

OpenMP API 规范的现在和未来

starIconstarIconemptyStarIconemptyStarIconemptyStarIcon

2.00/5 (1投票)

2017年4月17日

CPOL

21分钟阅读

viewsIcon

16942

这个黄金标准的并行编程语言是如何随着每个新版本而改进的。

点击此处注册并下载免费的 Intel® Parallel Studio XE 30天试用版.

Michael Klemm,英特尔德国有限公司高级员工应用工程师;Alejandro Duran,英特尔西班牙公司应用工程师;Ravi Narayanaswamy,高级员工开发工程师;Xinmin Tian,高级首席工程师;以及 Terry Wilmarth,英特尔公司高级开发工程师。

OpenMP* API 规范已有二十年的历史,自成立以来,OpenMP 功能不断增加,以跟上硬件和软件的发展,确保您可以使用它来编程您拥有的硬件。自 2013 年发布 4.0 版本以来,OpenMP 语言支持异构和 SIMD 编程。同样,2008 年通过添加任务构造改进了对具有不规则并行性的程序的®支持。OpenMP 技术报告 4:版本 5.0 预览 1(简称 TR4)是 OpenMP 语言演进的下一步。它增加了任务规约,扩展了 SIMD 并行编程,并大大提高了异构编程的生产力。在本文中,我们将回顾现有的 OpenMP 功能,并预览 TR4 支持的实现即将推出的功能。

任务:用任务表达自己

任务或基于任务的编程对于需要不规则并行性的应用程序(例如,递归算法、图遍历和对非结构化数据操作的算法)来说是一个重要的概念。自 OpenMP 3.0 版本以来,任务构造提供了一种便捷的方式来表达交给 OpenMP 运行时系统中的调度器的小型工作单元的并发执行。

void taskloop_example() {
#pragma omp taskgroup
    {
#pragma omp task
        long_running_task()  // can execute concurrently

#pragma omp taskloop collapse(2) grainsize(500) nogroup
        for (int i = 0; i < N; i++)
            for (int j = 0; j < M; j++)
                loop_body();
   }
}
图 1. 使用新的 taskloop 构造和 OpenMP* 任务的简单示例

图 1 说明了创建一个 OpenMP 任务来执行一个长时间运行的函数,然后是一个使用 `taskloop` 构造并行化的循环。这个构造出现在 OpenMP 4.5 中,并提供了语法糖,允许程序员使用 OpenMP 任务轻松并行化循环。它将循环迭代空间分成块,并为每个块创建一个任务。该构造支持多个子句以允许精细控制(例如,`grainsize` 用于控制每个任务的工作量,`collapse` 用于从 `i` 和 `j` 循环创建产品循环)。TR4 通过为 `taskgroup`、`task` 和 `taskloop` 构造定义新的子句来扩展 OpenMP 任务的表达能力,以在生成的任务之间执行归约。

图 2 说明了创建任务来处理链表并找到链表中所有元素的最小值。`parallel` 构造创建了一个并行区域,以便有工作线程可用于任务执行。`single` 构造然后将执行限制为一个线程,该线程遍历链表并通过 `omp task` 为每个列表项生成一个任务。这是在 OpenMP 中实现生产者-消费者模式的常见方式。

TR4 中的任务归约使用 OpenMP 4.0 版本中引入的 `taskgroup` 构造。它旨在逻辑地分组任务并提供一种等待组中所有任务完成的方法。TR4 通过 `task_reduction` 子句扩展了 `taskgroup` 构造以执行归约,如图 2 所示。如果将此子句添加到构造中,则由各个任务收集的所有部分结果将在 `taskgroup` 区域结束时聚合以形成最终结果。参与归约操作的任务必须具有与 `taskgroup` 归约子句匹配的 `in_reduction` 子句。

从 TR4 开始,`taskloop` 构造支持 `reduction` 和 `in_reduction` 子句及其任务归约语义。如果 `taskloop` 构造上出现 `reduction` 子句,则会创建一个隐式任务组,该任务组在循环结束时执行请求的归约操作。如果添加 `in_reduction` 子句,则由 `taskloop` 构造生成的任务将参与外部 `taskgroup` 区域的归约。

int find_minimum(list_t * list) {
    int minimum = INT_MAX;
    list_t * ptr = list;
#pragma omp parallel 
#pragma omp single
#pragma omp taskgroup task_reduction(min:minimum)
    {
        for (ptr = list; ptr ; ptr = ptr->next) {
#pragma omp task firstprivate(ptr) in_reduction(min:minimum)
            {
                int element = ptr->element;
                minimum = (element < minimum) ? element : minimum;
            }
        }
    }
    return minimum;
}
图 2. 遍历链表并使用任务归约计算最小值

卸载:充分利用协处理器

OpenMP API 致力于根据用户反馈改进卸载编译指示的可用性。为此,TR4 中添加了新功能,并增强了一些现有功能。其中一个关键的新功能是能够自动检测卸载区域中使用的函数,并将它们视为出现在 `declare target` 指令中。以前,卸载区域中调用的所有函数都必须使用 `declare target` 指令显式标记。这是一项艰巨的工作,特别是如果例程位于程序员不拥有的头文件中(例如,标准模板库),并且需要对头文件本身进行 `declare target` 指令,这会为设备创建头文件中每个函数的副本,即使某些函数未在卸载区域中使用。

#pragma omp declare target
void foo() {
    // ...
}
#pragma omp end declare target

void bar()  {
#pragma omp target
    {
        foo();
    }
}
void foo() {
    // ...
}



void bar() {
#pragma omp target
    {
        foo();
    }
}
图 3. 自动检测卸载区域中使用的函数

图 3 中,左侧代码显示了 OpenMP 4.5 版本所需的功能。从 TR4 开始,由于设备函数的隐式检测和创建,右侧代码就足够了。

TR4 中,自动检测功能还扩展到具有静态存储持续时间的变量。图 4 中的示例是等效的。

int x;
#pragma omp declare target to (x)

void bar() {
    #pragma omp target
    {
        x = 5;
    }
}
int x;


void bar() {
    #pragma omp target
    {
        x = 5;
    }
}
图 4. 静态存储期变量的自动检测

OpenMP 4.5 版本引入了 `use_device_ptr` 子句。`use_device_ptr` 中的变量在使用之前必须进行映射。为此,程序员需要使用单独的 `#pragma target data` 子句,因为一个变量只能出现在一个数据子句中。因此,图 5 中需要 OpenMP 指令。

#pragma omp target data map(buf)
#pragma omp target data use_device_ptr(buf)
图 5. 映射变量

在 TR4 中,有一个例外,即变量可以同时出现在 `map` 和 `use_device_ptr` 子句中,在一个构造中,如图 6 所示。

#pragma omp target data map(buf) use_device_ptr(buf)
图 6. 变量可同时出现在 map 和 use_device_ptr 子句中

静态数据成员现在允许出现在 `omp declare target` 构造内的类中。具有静态成员的类对象也可以用于 `map` 子句(图 7)。

#pragma omp declare target
class C {
    static int x;
    int y;
}
class C  myclass;
#pragma omp end declare target

void bar() {
#pragma omp target map(myclass)
    {
        myclass.x = 10
    }
}
图 7. 具有静态成员的类对象用于 map 子句

此外,虚拟成员函数允许出现在 `omp declare target` 构造内的类中,或用于 `map` 子句的对象中。唯一的注意事项是,如果对象是在同一设备上创建的,则虚拟成员函数只能在该设备上调用。

在 OpenMP 4.5 中,用于组合构造(其中第一个构造是 target)的 `reduction` 或 `lastprivate` 子句中的标量变量被视为 target 构造的 `firstprivate`。这导致主机值从未更新,令人惊讶。为了更新主机上的值,程序员必须将 `omp target` 指令与组合构造分开,并显式映射标量变量。在 TR4 中,此类变量会自动视为已对其应用 `map(tofrom:variable)`。

如果使用 `omp target data` 映射命名数组的一个部分,则 `omp target data` 构造内任何引用该数组的嵌套 `omp target` 将需要隐式映射到该数组在外部 `omp target data` 映射子句中使用的相同部分或子部分。如果内部 `omp target` 区域省略了显式映射,则隐式映射规则将生效,这意味着整个数组将根据 OpenMP 4.5 版本进行映射。这将导致运行时错误,因为当数组的一个子部分已映射时,却映射了更大尺寸的数组。同样,在外部 `omp target data` 构造中映射结构变量的一个字段,并在嵌套的 `omp target` 构造中使用结构变量的地址,将导致尝试映射整个结构变量,而该结构的一部分已被映射。TR4 修复了这些情况,以提供程序员通常期望的行为(图 8)。

struct {int x,y,z} st;	
int A[100];	
#pragma omp target data map(s.x A[10:50])	
{
#pragma omp target	
    {
        A[20] = ;       // error in OpenMP 4.5, Ok in TR4	
        foo(&st);       // error in OpenMP 4.5, OK in TR4
    }
#pragma omp target map(s.x, A[10:50])	
    {
        A[20] = ;       // Ok OpenMP 4.5 and TR4	
        foo(&st);       // Ok OpenMP 4.5 and TR4
    }
}
图 8. 改进的映射

TR4 中的新功能改进了使用 OpenMP 进行卸载的可编程性,减少了对应用程序的修改。对 `target` 区域中使用的变量和函数的自动检测消除了显式指定的需求。同样,消除了在嵌套区域中重复映射子句的需要,并允许变量同时出现在 `map` 和 `use_device_ptr` 中,从而减少了所需的 OpenMP 指令数量。对归约变量行为的更改使语言与程序员的期望保持一致。总的来说,更清晰的语义使得在 OpenMP 应用程序中使用卸载设备变得更简单、更直观。

高效的 SIMD 编程

具有跨迭代依赖的 SIMD 循环

OpenMP 4.5 版本通过添加新的 SIMD 子句扩展了有序构造。有序 SIMD 构造声明 SIMD 循环或 SIMD 函数中的结构化块必须分别按迭代顺序或函数调用顺序执行。图 9 显示了使用有序 SIMD 块来保留每次迭代内部和迭代之间的读写、写读和写写顺序,而整个循环可以使用 SIMD 指令并发执行。在第一个有序 `simd` 块中,数组 `a` 的索引 `ind[i]` 可能存在写写冲突(例如,`ind[0] = 2`,`ind[2] = 2`),因此它需要通过 `ordered simd` 进行序列化以允许整个循环的向量化。在第二个 `ordered simd` 块中,`myLock(L)` 和 `myUnlock(L)` 操作必须在一个有序 SIMD 块中。否则,作为循环向量化的一部分(例如,向量长度为二),对 `myLock(L)` 和 `myLock(L)` 的调用将扩展为两个调用,如下所示:`{myLock(L); myLock(L); …; myUnlock(L); myUnlock(L);}`。嵌套锁函数通常会导致死锁。示例中所示的有序 SIMD 构造创建了正确的序列 `{myLock(L); …; myUnlock(L); …; myLock(L); myUnlock(L);}`。

#pragma omp simd
for (i = 0; i < N; i++) {
    // ...
#pragma omp ordered simd
    {
        // write-write conflict
        a[ind[i]] += b[i]; 
    }
    // ...
#pragma omp ordered simd
    {
       // atomic update 
       myLock(L)          
       if (x > 10) x = 0;
       myUnlock(L)
    }
    // ...
}
#pragma omp simd
for (i = 0; i < N; i++) {
    // ...
#pragma omp ordered simd  
    {
        if (c[i]) > 0) q[j++] = b[i];    
    }
    // ...
#pragma omp ordered simd
    {                     
        if (c[i] > 0) q[j++] = d[i];                   
    }
    // ...
}
图 9. 保持每次迭代内部和迭代之间的读写、写读和写写顺序

在 `ordered` 构造上使用 `simd` 子句时,需要小心,不要违反两个 `ordered simd` 块之间固有的依赖关系。图 9 显示了 `\#pragma omp ordered simd` 的不正确用法,因为在 SIMD 执行下,存储顺序相对于其串行执行发生了改变。假设 `c[0] = true` 和 `c[1] = true`。当上述循环串行执行时,存储顺序是:`q[0] = b[0], q[1] = d[0], q[2] = b[1], q[3] = d[1]`,依此类推。然而,当循环以向量长度为二并发执行时,存储顺序是:`q[0] = b[0], q[1] = b[1], q[2] = d[0], q[3] = d[1]`,... 存储顺序的改变是由于循环中两个 `ordered simd` 块之间变量 `j` 的写读依赖关系被违反。正确的用法是将两个 `ordered simd` 块合并为一个 `ordered simd` 块。

Linear 子句的 REF/UVAL/VAL 修饰符扩展

`linear` 子句提供了 `private` 子句功能的超集。当在构造上指定 `linear` 子句时,关联循环每次迭代中新列表项的值对应于进入构造前原始列表项的值,加上逻辑迭代次数乘以线性步长。关联循环的顺序最后一次迭代对应的值被分配给原始列表项。当在声明性指令上指定 `linear` 子句时,所有列表项必须是函数的形参(或 Fortran 中的虚拟参数),该函数将在每个 SIMD 通道上并发调用。

向 `linear` 子句添加 `ref`/`uval`/`val` 修饰符的理由是为程序员提供一种方法,可以精确地指定内存引用相对于地址和数据值的 `linear` 或 `uniform` 属性,以便编译器可以利用这些信息生成高效的 SIMD 代码,使用单位步长加载/存储而不是聚集/分散。本质上,对于隐式引用的线性参数,最好将引用视为线性。`uval`/`val`/`ref` 的语义描述如下:

  • `linear(val(var):[step])` 表示即使变量通过引用传递,其值也是线性的。对于通过引用传递的变量,传递的是地址向量。在这种情况下,编译器必须生成 gathers 或 scatters。
  • `linear(uval(var):[step])` 表示通过引用传递的值是线性的,而引用本身是统一的。因此,传递给第一个通道的引用,但其他值可以使用步长构建。编译器可以使用通用寄存器传递基地址并计算其线性值。
  • `linear(ref(var):step)` 表示参数通过引用传递,底层引用是线性的,并且内存访问将是线性单位步长或非步长,具体取决于步长。编译器可以使用通用寄存器传递基地址并计算其线性地址。

图 10 展示了一个名为 FOO 的函数,其参数 X 和 Y 在 Fortran 中是按引用传递的。“`VALUE`”属性不会改变此行为。它仅表示根据 Fortran 2008 语言规范,更新后的值对调用者不可见。由于 X 和 Y 的引用未被标记为线性,编译器必须生成 gather 指令来加载 (X0, X1, X2, X3) 和 (Y0, Y1, Y2, Y3),假设向量长度为四。在图 11 中,X 和 Y 的引用被标记为线性,因此编译器可以生成单位步长 SIMD 加载,从而获得更好的性能。

      REAL FUNCTION FOO(X, Y)
!$omp declare simd(FOO)  
      REAL, VALUE :: Y    !! pass by reference 
      REAL, VALUE :: X    !! pass by reference
      FOO = X + Y         !! gathers generated
                          !! based on vector 
                          !! of addresses  
      END FUNCTION FOO
      ! ... 
!omp$ simd private(X,Y)
      DO I= 0, N
        Y = B(I)
        X = A(I)
        C(I) += FOO(X, Y)
      ENDDO
图 10. 编译时引用 X 和 Y 的线性度未知
      REAL FUNCTION FOO(X, Y)
!$omp declare simd(FOO) linear(ref(X), ref(Y))
      REAL, VALUE :: Y    !! pass by reference
      REAL, VALUE :: X    !! pass by reference 
      FOO = X + Y         !! unit stride 
                          !! SIMD loads
      END FUNCTION FOO
      ! ...
!omp$ simd private(X,Y)
      DO I= 0, N
        Y = B(I)
        X = A(I)
        C(I) += FOO(X, Y)
      ENDDO
图 11. X 和 Y 的引用被标记为线性

图 12 中,函数 `add_one` 被标注为 SIMD 函数。它有一个 C++ 引用参数 `const int &p`。假设向量长度为四,如果 `p` 被标注为 `linear(ref(p))`,编译器可以使用 `rax` 寄存器中的基地址 `p` 生成单位步长加载指令,以将 `p[0]`、`p[1]`、`p[2]` 和 `p[3]` 加载到 `xmm0` 寄存器。在这种情况下,`add_one` 函数只需要三条指令。

#pragma omp declare simd notinbranch // linear(ref(p))
__declspec(noinline)
int add_one(const int& p) {
    return (p + 1);
}
图 12. 带有和不带有 linear(ref(p)) 注释的 SIMD 代码比较

然而,如果 `p` 没有被标注为 `linear(ref(p))`,编译器必须假定通过两个 `xmm` 寄存器传递了四个不同的地址 `p0`、`p1`、`p2` 和 `p3`,并且 gather 操作是通过一系列标量加载和打包指令模拟的。结果,`add_one` 函数现在需要 16 条指令而不是三条。

总而言之,OpenMP 4.5 版本中新增的 SIMD 功能允许用户向编译器提供更多信息,从而在许多情况下可以向量化更多的循环并生成更好的向量代码。

亲和性:轻松实现线程放置

OpenMP 4.0 规范首次为用户提供了控制线程亲和性的标准方法。它向语言引入了两个新概念:

  1. 绑定策略
  2. 位置分区

由 `bind-var` 内部控制变量 (ICV) 指定的绑定策略,决定了线程组的线程相对于其父线程位置的绑定位置。由 `place-partition-var` ICV 指定的位置分区,是线程可以绑定的位置集合。一旦线程绑定到给定组的位置,就不应将其从该位置移动。

规范定义了三种绑定策略:`master`、`close` 和 `spread`。在描述这些策略时,我们将考虑四个位置,每个位置都是一个具有两个线程的核。我们将展示在这些位置放置三个线程和六个线程的示例,并假设父线程将始终位于第三个位置。在 `master` 策略中,主线程绑定到父线程的位置,然后团队中剩余的线程被分配到与主线程相同的位置(表 1)。

表 1. master 策略的线程放置

 

位置 1: {0,1}

位置 2: {2,3}

位置 3: {4,5} (父线程)位置 4: {6,7}
三个线程

 

 

0, 1, 2

 

六个线程

 

 

0, 1, 2, 3, 4, 5

 

`close` 策略首先将主线程放置在父线程的位置,然后以循环方式处理团队中剩余的线程。要在 P 个位置放置 T 个线程,主线程的位置大约获得前 T/P 个线程,然后位置分区中的下一个位置获得接下来的 T/P 个线程,依此类推,根据需要环绕位置分区,给出分布(表 2)。

表 2. close 策略的线程放置

 

位置 1: {0,1}位置 2: {2,3}位置 3: {4,5} (父线程)位置 4: {6,7}
三个线程2

 

01
六个线程450,12,3

使用 `spread` 策略,事情变得非常有趣。线程的放置方式将使其在可用位置上分散开。这通过形成 `T` 个大致均匀的位置分区子分区,或者如果 `T >= P` 则形成 `P` 个分区来实现。如果 `T <= P`,则每个线程获得自己的子分区,从主线程开始,它将获得包含父线程绑定位置的子分区。每个后续线程绑定到每个后续子分区的第一个位置,根据需要环绕。如果 `T > P`,则连续的线程组获得相同的子分区,在这种情况下将由单个位置组成。因此,集合中的所有线程将绑定到同一个位置。我们在表 3 的花括号中显示了形成的子分区。如果使用嵌套并行性,这些很重要,因为它们影响每个嵌套并行区域使用的可用资源。

表 3. spread 策略的线程放置和子分区

 

位置 1: {0,1}位置 2: {2,3}位置 3: {4,5} (父线程)位置 4: {6,7}
三个线程1 {{0,1}}2 {{2,3}}0 {{4,5},{6,7}} 注意:0 绑定到 {4,5}
六个线程4 {{0,1}}5 {{2,3}}0,1 {{4,5}}2,3 {{6,7}}

OpenMP 4.0 版本还提供了线程亲和绑定策略的查询函数:`omp_proc_bind_t omp_get_proc_bind()`。它返回下一个 `parallel` 区域要使用的绑定策略(假设该区域未指定 `proc_bind` 子句)。

关于 `spread` 策略真正有趣的是子分区会发生什么。使用 `master` 和 `close` 策略,每个隐式任务继承父隐式任务的位置分区。但是,在 `spread` 策略中,隐式任务会将其 `place-partition-var` ICV 设置为子分区。这意味着嵌套的 `parallel` 构造将将其所有线程放置在其父级的子分区中。

bind-var 的值可以通过环境变量 `OMP_PROC_BIND` 初始化。bind-var 的值也可以通过向 `parallel` 构造添加 `proc_bind` 子句来覆盖。指定 `place-partition-var` 是通过 `OMP_PLACES` 环境变量完成的。位置可以是硬件线程、核心、套接字或这些的特定数量。它们也可以是显式处理器列表。更多详细信息可在 OpenMP API 规范中找到。

OpenMP 4.5 规范通过提供一组能够查询当前线程的位置分区和绑定位置方面的函数,增强了语言的亲和能力。这些新的 API 函数对于确认设置的正确性以实现程序员期望的线程亲和性非常有用。当代码复杂性很高且嵌套并行与 `spread` 绑定策略结合使用以将线程放置在嵌套并行区域中,使它们共享较低级别缓存时,这一点尤其重要。这些 API 函数是:

  • `int omp_get_num_places()`:返回初始任务执行环境中 `place-partition-var` 中的位置数量。
  • `int omp_get_place_num_procs(int place_num)`:返回位置分区中由 `place_num` 指定位置的执行环境可用的处理器数量。
  • `void omp_get_place_proc_ids(int place_num, int *ids)`:获取位置分区中由 `place_num` 指定位置的执行环境可用的处理器,分配一个数组来保存它们,并将该数组放在 `ids` 处。
  • `int omp_get_place_num(void)`:返回当前线程绑定到的位置分区中的位置编号。
  • `int omp_get_partition_num_places(void)`:返回最内层隐式任务的位置分区中的位置数量。请注意,这与 `omp_get_num_places()` 不同,因为它将显示 `spread` 绑定策略在位置分区被分解为子分区时的效果,而 `omp_get_num_places()` 将始终显示完整的原始位置分区。
  • `void omp_get_partition_place_nums(int *place_nums)`:获取对应于最内层隐式任务的位置分区的位置编号列表,并在 `place_nums` 中分配一个数组来存储它们。请注意,位置编号是完整原始位置分区中的位置编号。此函数对于查看原始位置分区中的哪些位置出现在使用 `spread` 绑定策略导致的子分区中特别有用。

OpenMP API 规范 5.0 展望

OpenMP 架构审查委员会 (ARB) 正在讨论可能出现在 OpenMP 规范 5.0 版本中的其他功能。本节描述了最有可能的候选者。

内存管理支持

如何在 OpenMP 应用程序中支持日益复杂的内存层次结构是一个活跃讨论的领域。这种复杂性来自多个方面:具有不同特性(例如 Intel® Xeon Phi™ 处理器上的 MCDRAM 或 Intel® 3D XPoint™ 内存)的新内存,需要请求分配内存的某些特性以确保良好性能(例如,某些对齐或页面大小),某些内存需要特殊的编译器支持,NUMA 效应等等。此外,许多新的内存技术正在研究中,因此任何提案都需要具有可扩展性以处理未来的技术。

当前的工作方向基于两个关键概念,它们试图模拟不同的技术和操作:内存空间和分配器。内存空间表示具有一组特征(例如,页面大小、容量、带宽等)的系统内存,程序员可以指定这些特征来找到他们希望程序使用的内存。分配器是分配内存空间中内存的对象,也可以具有改变其行为的特征(例如,分配的对齐方式)。

定义了新的 API 来操作内存空间和分配器,以及分配和释放内存。将创建分配器和分配内存的调用分开,允许构建可维护的接口,其中关于内存应在何处分配的决策是在一个通用的“决策”模块中做出的。图 13 显示了如何使用该提案来选择系统中具有最高带宽并使用 2MB 页面的内存,并从该内存中定义两个不同的分配器:一个确保分配是 64 字节对齐的,另一个则不确保。

omp_memtrait_set_t  trait_set;
omp_memtrait_t  traits[] = {{OMP_MTK_BANDWIDTH,OMP_MTK_HIGHEST}, 
                            {OMP_MTK_PAGESIZE, 2*1024*1024}}; 
omp_init_memtrait_set(&trait_set,2,traits);

omp_memspace_t *amemspace = omp_init_memspace(&trait_set);

omp_alloctrait_t trait = {{OMP_ATK_ALIGNMENT},{64}};
omp_alloctrait_set_t trait_set;
omp_init_alloctrait_set(&trait_set,1,&trait);

omp_allocator_t *aligned_allocator = omp_init_allocator(amemspace,  
                                                        &trait_set);
omp_allocator_t *unaligned_allocator = omp_init_allocator(amemspace, NULL);

double *a = (double *) omp_alloc( aligned_allocator, N * sizeof(double) );
double *b = (double *)omp_alloc( unaligned_allocator, N * sizeof(double) );
图 13. 选择带宽最高的内存

提出了新的 `allocate` 指令来影响未通过 API 调用(例如,自动或静态变量)分配的变量的底层分配。新的 `allocate` 子句可用于影响 OpenMP 指令进行的分配(例如,变量的私有副本)。图 14 显示了如何使用该指令将变量 `a` 和 `b` 的分配更改为具有最高带宽且也使用 2 MB 页面的内存。示例中并行区域中 `b` 的私有副本分配在具有最低延迟的内存上。

int a[N], b[M];
#pragma omp allocate(a,b) memtraits(bandwidth=highest, pagesize=2*1024*1024)

void example() {
#pragma omp parallel private(b) allocate(memtraits(latency=lowest):b)
    {
        // ...
    }
}
图 14. 将变量 a 和 b 的分配更改为具有最高带宽且也使用 2MB 页面的内存

异构编程的改进

正在考虑多项功能以改进 OpenMP 的设备支持:

  • 目前,`map` 子句中的结构按位复制,包括结构中的指针字段。如果程序员要求指针字段指向有效的设备内存,则需要创建设备内存并显式更新设备上的指针字段。委员会正在讨论扩展,通过扩展 `map` 子句以支持结构中的指针字段,使程序员能够指定结构中指针字段的自动附加/分离。
  • ARB 正在考虑允许函数指针在 `target` 区域中使用,并允许函数指针出现在 `declare target` 中。
  • 可以异步执行的新设备 `memcpy` 例程。
  • 支持启用 `target` 构造的“在设备上执行或失败”语义。目前,当设备不可用时,`target` 区域可以在主机上静默执行。
  • 支持仅存在于设备上而非基于主机的变量和函数。
  • 支持单个应用程序中的多种设备类型。

任务改进

除了本文讨论的功能之外,OpenMP 5.0 还在考虑其他功能,例如:

  • 实现 `taskloop` 构造之间的数据依赖。
  • 允许 `task` 构造和 `taskloop` 构造中的数据依赖包含可扩展为多个值的表达式,以从单个 `depend` 子句生成多个依赖。
  • 提供支持,以类似 `OMP_PROC_BIND` 的模式表达任务到线程的亲和性。

其他更改

OpenMP 5.0 其他活跃讨论中的功能包括:

  • 将 OpenMP 基础语言规范升级到 C11、C++11 或 C++14 以及 Fortran 2008。
  • 放宽 `collapse` 子句的限制,允许非矩形循环形状,并允许代码出现在嵌套循环之间。
  • 允许归约在并行区域中间发生,而无需与工作共享构造相关联。

OpenMP API 是高性能计算及其他领域中 C、C++ 和 Fortran 应用程序共享内存并行化的可移植且与供应商无关的并行编程语言的黄金标准。随着 5.0 版本即将推出的发展,它有望为开发人员提供更多功能,以充分利用现代处理器的能力。

© . All rights reserved.