OpenMP

目录

什么是OpenMP

  • OpenMP全称是”Open specification for Multi-Processing“,它提供了一组用于并行处理的API,跟pthreads一样,适用于share memory场景。由编译器来生成多线程处理的代码,优点是实现简单,缺点是不如pthreads灵活。OpenMP支持fork-join模型,默认提供了join操作,所有线程执行结束了才会返回到主线程。如果其中一个线程异常终止,所有线程都会终止。

支持多种指令集和操作系统,由非营利性组织管理,多家软硬件厂家参与,包括Arm,AMD,Intel等。

  • 版本演进

查看OpenMP版本

echo |cpp -fopenmp -dM |grep -i open

先来看一个OpenMP的例子:

#include <omp.h>

int A[10],B[10],C[10];

// Beginning of parallel section. Fork a team of threads
#pragma omp parallel for num_threads(10)

{
	for (int i=0; i<10; ++i)
		A[i] = B[i]+C[i];
} // All threads join master thread and terminate
  • 可以看到OpenMP指令包含下面三个部分:
    • #pragma omp
    • directive-name(指令):用哪一种方式做并行(parallel,do,for)
    • [clause…] (从句): 可选

Parallel Region Construct:并行区构造

parallel指令会创建一组线程,并行执行并行区中的代码

#pragma omp parallel [clause...] if (scalar_expression) num_threads (integer-expression)

{
    Parallel Region
}
  • 支持的从句有:
    • if(scalar_expression): 决定是否以并行方式执行并行区
      • True:并行
      • False:串行,一个线程
    • num_threads(integer_expression): 指定并行区的线程数
    • defalut(shared,none):指定默认
      • shared:默认为共享变量
      • none:无默认变量类型,每个变量都需要另外指定
    • shared(list): 指定共享变量列表变量类型
      • 共享变量在内存中只有一份,所有线程都可以访问
      • 不特别指定,并行区变量默认为shared
      • 请保证共享访问不会冲突
    • private(list): 指定私有变量列表
      • 每个线程生成一份与该私有变量同类型的数据对象
      • 变量需要重新初始化
    • firstprivate(list):
      • 同private
      • 对变量根据主线程中的数据进行初始化
    • reduction:把每个线程的数据收集起来,然后做操作,效率较高
    #pragma omp parallel for num_threads(8) reduction(+:ans_omp)
      
    for (int i=0; i<N; i++)
        ans_omp += b[i] * b[i];
    ans_omp = sqrt(ans_omp);
    
  • 可以通过以下方式确定线程数,优先级从高到低:
    1. 设定IF从句;
    2. 设定num_threads从句;
    3. 在进入并行区之前调用库函数omp_set_num_threads()
    4. 设置OMP_NUM_THREADS环境变量
    5. 默认使用CPU的核数
  • 并行区可以嵌套

  • 并行区的代码有限制

Work-Sharing Construct:共享工作构造

  • 共享工作构造指的是把工作进行分解,然后分配给不同的线程。主要有以下三种类型:

Do/for构造

适用于循环(do-while,for),用来处理数据并行问题

不能创建线程,因此要跟parallel一起使用

for循环需要满足格式要求

  • 支持的从句:
    • nowait: Do not synchronize threads at the end of the loop
    • schedule: Describes how iterations are divided among threads
      • STATIC
      • DYNAMIC: 一个线程执行完一个chunk的任务,会被动态分配另一个chunk
      • GUIDED: similar to DYNAMIC except chunk size decreases over time
      • RUNTIME: runtime的时候根据OMP_SCHEDULE决定用上面三个中的哪一个调度
      • AUTO: compiler决定,效果不太好?
int chunk = CHUNKSIZE;
int thread = NUM_THREAD;
int N = 1000;
#pragma omp parallel num_thread(thread) shared(a,b,c) private(i)

{
    #pramga omp for schedule(dynamic, chunk) nowait
    for(int i=0; i<N; ++i)
        c[i] = a[i] + b[i];
}
  • ordered: Iterations must be executed as in a serial program
    • 会降低程序运行效率
  • collapse: 表示紧随其后的 n 层循环会被合并然后并行化
    • https://blog.csdn.net/qq_37206769/article/details/89189780

SECTIONS构造

将并行区内的代码块划分为多个section分配执行,适用于non-iterative任务

  • 每个section由一个thread执行一次
    • 线程数大于section数目:部分线程空闲
    • 线程数小于section数目:部分线程分配多个section

可以搭配parallel合成为parallel sections构造

#pragma omp sections [clause...]

{
  #pragma omp section
  	structred_block
  	
  #pragma omp section
  	structred_block 	
}

Single构造

Synchronization Constructs:同步构造

  • 并行区的执行不是原子的,因此不同线程的操作可能会混在一起。如果需要数据同步,可以进行同步构造。

任务(task)构造

默认的构造都遵循Fork-Join模式,对任务类型由限制。

允许定义任务及依赖关系,动态调度执行,即动态管理线程池和任务池。

OpenMP in C++

cmake编译

find_package(OpenMP)
add_compile_options(-Wunknown-pragmas)

add_executable(hello src/hello.cpp)
target_link_libraries(hello OpenMP::OpenMP_CXX)

常用库函数

// 设置并行区运行的线程数
void omp_set_num_threads(int)
// 获得并行区运行的线程数
int omp_get_num_threads(void)
// 获得线程编号
int omp_get_thread_num(void)
// 获得openmp wall clock时间(秒)
double omp_get_wtime(void)
// 获得omp_get_wtime时间精度
double omp_get_wtick(void)