1、摘要
OpenMP 是一个应用程序接口(API),由一组主要的计算机硬件和软件供应商联合定义。OpenMP 为共享内存并行应用程序的开发人员提供了一个可移植的、可伸缩的模型。该API在多种体系结构上支持 C/C++ 和 Fortran。本教程涵盖了 OpenMP 3.1 的大部分主要特性,包括用于指定并行区域、工作共享、同步和数据环境的各种构造和指令。还将讨论运行时库函数和环境变量。本教程包括 C 和 Fortran 示例代码以及一个实验练习。
水平/先决条件:本教程非常适合那些刚接触 OpenMP 并行编程的人。需要对 C 语言或 Fortran 语言中的并行编程有基本的了解。对于那些通常不熟悉并行编程的人来说,EC3500: 并行计算导论中的材料将会很有帮助。
2、简介
2.1、什么是OpenMP
OpenMP是:
一种应用程序接口(API),可用于显式地指示多线程、共享内存并行性。
由三个主要的API组件组成:
编译器指令
运行时库函数
环境变量
Open Multi-Processing的缩写
OpenMP的目标
标准化
在各种共享内存架构/平台之间提供一个标准。
由一组主要的计算机硬件和软件供应商联合定义和认可。
至精至简
为共享内存机器建立一组简单且有限的指令。
重要的并行性可以通过使用3或4个指令来实现。
显然,随着每个新版本的发布,这个目标变得越来越没有意义。
易用性
提供以增量方式并行化串行程序的能力,这与通常需要全有或全无方法的消息传递库不同。
提供实现粗粒度和细粒度并行的能力。
可移植性
为 C/C++ 和 FORTRAN 指定API。
大多数主要平台已经实现,包括Unix/Linux平台和Windows。
注意:本教程参考了OpenMP 3.1版。新版本的语法和特性目前还没有涉及。
3、OpenMP编程模型
3.1、共享内存模型
OpenMP是为多处理器或多核共享内存机器设计的。底层架构可以是共享内存 UMA 或 NUMA。
Uniform Memory Access 一致内存访问
Uniform Memory Access 非一致内存访问
因为OpenMP是为共享内存并行编程而设计的,所以它在很大程度上局限于单节点并行性。通常,节点上处理元素(核心)的数量决定了可以实现多少并行性。
3.2、在 HPC 中使用 OpenMP 的动机
OpenMP本身的并行性仅限于单个节点。
对于高性能计算(HPC - High Performance Computing)应用程序,OpenMP 与 MPI 相结合以实现分布式内存并行。这通常被称为混合并行编程。
OpenMP 用于每个节点上的计算密集型工作。
MPI 用于实现节点之间的通信和数据共享。
这使得并行性可以在集群的整个范围内实现。
Hybrid OpenMP-MPI Parallelism
3.3、基于线程的并行性
OpenMP 程序仅通过使用线程来实现并行性。
执行线程是操作系统可以调度的最小处理单元。一种可以自动运行的子程序,这个概念可能有助于解释什么是线程。
线程存在于单个进程的资源中。没有这个进程,它们就不复存在。
通常,线程的数量与机器处理器/核心的数量相匹配。但是,线程的实际使用取决于应用程序。
3.4、显式并行性
OpenMP 是一个显式的(而不是自动的)编程模型,为程序员提供了对并行化的完全控制。
并行化可以像获取串行程序和插入编译器指令一样简单…
或者像插入子程序来设置多个并行级别、锁甚至嵌套锁一样复杂
3.5、Fork - Join 模型
OpenMP 使用并行执行的 fork-join 模型:
所有 OpenMP 程序都开始于一个主线程。主线程按顺序执行,直到遇到第一个并行区域结构。
FORK:主线程然后创建一组并行线程。
之后程序中由并行区域结构封装的语句在各个团队线程中并行执行。
JOIN:当团队线程完成并行区域结构中的语句时,它们将进行同步并终止,只留下主线程。
并行区域的数量和组成它们的线程是任意的。
3.6、数据范围
因为 OpenMP 是共享内存编程模型,所以在默认情况下,并行区域中的大多数数据都是共享的。
一个并行区域中的所有线程都可以同时访问共享数据。
OpenMP 为程序员提供了一种方法,可以在不需要默认共享范围的情况下显式地指定数据的“作用域”。
数据范围属性子句将更详细地讨论这个主题。
3.7、嵌套的并行性
该 API 提供了在其他并行区域内放置并行区域的方法。
实现可能支持这个特性,也可能不支持。
3.8、动态线程
该 API 为运行时环境提供了动态更改线程数量的功能,这些线程用于执行并行区域。如有可能,旨在促进更有效地利用资源。
实现可能支持这个特性,也可能不支持。
3.9、I/O
OpenMP 没有指定任何关于并行 I/O 的内容。如果多个线程试图从同一个文件进行写/读操作,这一点尤其重要。
如果每个线程都对不同的文件执行 I/O,那么问题就不那么重要了。
完全由程序员来确保在多线程程序的上下文中正确地执行 I/O。
3.10、内存模型:经常刷新?
OpenMP 提供了线程内存的“宽松一致性”和“临时”视图(用他们的话说)。换句话说,线程可以“缓存”它们的数据,并且不需要始终与实际内存保持精确的一致性。
当所有线程以相同的方式查看共享变量非常重要时,程序员负责确保所有线程根据需要刷新该变量。
4、OpenMP API 概述
4.1、三大构成
OpenMP 3.1 API 由三个不同的组件组成:
编译器指令
运行时库函数
环境变量
后来的一些 API 包含了这三个相同的组件,但是增加了指令、运行时库函数和环境变量的数量。
应用程序开发人员决定如何使用这些组件。在最简单的情况下,只需要其中的几个。
实现对所有 API 组件的支持各不相同。例如,一个实现可能声明它支持嵌套并行,但是 API 清楚地表明它可能被限制在一个线程上——主线程。不完全符合开发人员的期望?
4.2、编译器指令
编译器指令在源代码中以注释的形式出现,编译器会忽略它们,除非您另外告诉它们 — 通常通过指定适当的编译标志,如后面的编译部分所述。
OpenMP 编译器指令用于各种目的:
生成一个并行区域
在线程之间划分代码块
在线程之间分配循环迭代
序列化代码段
线程之间的工作同步
编译器指令有以下语法:
sentinel directive-name [clause, ...]
例如:
#pragma omp parallel default(shared) private(beta, pi)
后面将详细讨论编译器指令。
4.3、运行时库函数 Run-time Library Routines:
OpenMP API 包括越来越多的运行时库函数。
这些程序用于各种目的:
设置和查询线程的数量
查询线程的唯一标识符(线程ID)、父线程的标识符、线程团队大小
设置和查询动态线程特性
查询是否在一个并行区域,以及在什么级别
设置和查询嵌套并行性
设置、初始化和终止锁以及嵌套锁
查询 wall clock time 和分辨率
对于 C/C++,所有运行时库函数都是实际的子程序。对于Fortran来说,有些是函数,有些是子程序。例如:
#include
int omp_get_num_threads(void)
注意,对于C/C++,通常需要包含 头文件。
运行时库函数将在运行时库函数一节中作为概述进行简要讨论,更多细节将在附录A中讨论。
4.4、环境变量
OpenMP 提供了几个环境变量,用于在运行时控制并行代码的执行。
这些环境变量可以用来控制这些事情:
设置线程数
指定如何划分循环交互
将线程绑定到处理器
启用/禁用嵌套的并行性;设置嵌套并行度的最大级别
启用/禁用动态线程
设置线程堆栈大小
设置线程等待策略
设置 OpenMP 环境变量的方法与设置任何其他环境变量的方法相同,并且取决于您使用的是哪种 shell。例如:
csh/tcsh: setenv OMP_NUM_THREADS 8
sh/bash: export OMP_NUM_THREADS=8
稍后将在环境变量一节中讨论 OpenMP 环境变量。
4.5、OpenMP代码结构示例
#include
main () {
int var1, var2, var3;
串行代码 `Serial code`
.
.
.
并行区域的开始。派生一组线程。 `Beginning of parallel region. Fork a team of threads.`
指定变量作用域 `Specify variable scoping `
#pragma omp parallel private(var1, var2) shared(var3)
{
由所有线程执行的并行区域 `Parallel region executed by all threads`
.
其他 OpenMP 指令 `Other OpenMP directives`
.
运行时库调用 `Run-time Library calls`
.
所有线程加入主线程并解散 `All threads join master thread and disband`
}
恢复串行代码 `Resume serial code`
.
.
.
}
5、编译 OpenMP 程序
OpenMP 版本依赖的 GCC 版本
OpenMP 版本
GCC版本
OpenMP 5.0
>= GCC 9.1
OpenMP 4.5
>= GCC 6.1
OpenMP 4.0
>= GCC 4.9.0
OpenMP 3.1
>= GCC 4.7.0
OpenMP 3.0
>= GCC 4.4.0
OpenMP 2.5
>= GCC 4.2.0
g++ Test.cpp -o omptest -fopenmp
6、OpenMP 指令
6.1、C/C++ 指令格式
格式
#pragma omp
directive-name
[clause, ...]
newline
所有 OpenMP C/C++ 指令都需要。
一个有效的 OpenMP 指令。必须出现在 pragma 之后和任何子句之前。
可选的。除非另有限制,子句可以按任何顺序重复。
必需的。在此指令所包含的结构化块之前。
示例
#pragma omp parallel default(shared) private(beta, pi)
一般规则
区分大小写。
指令遵循 C/C++ 编译器指令标准的约定。
每个指令只能指定一个指令名。
每个指令最多应用于一个后续语句,该语句必须是一个结构化块。
长指令行可以通过在指令行的末尾使用反斜杠(“\”)来转义换行符,从而在后续的行中“继续”。
6.2、指令范围
静态(词法)范围
在指令后面的结构化块的开始和结束之间以文本形式封装的代码。
指令的静态范围不跨越多个程序或代码文件。
孤立的指令
一个 OpenMP 指令,独立于另一个封闭指令,称为孤立型指令。它存在于另一个指令的静态(词法)范围之外。
将跨越程序和可能的代码文件。
动态范围
指令的动态范围包括静态(词法)范围和孤立指