1. 说明
在上一篇分析CPU search过程的文档中,发现Faiss为提高搜索效率,缩短搜索时间,应用了OpenMP并行运算的功能,这一篇文档针对IndexIVF.cpp->search_preassigned函数中使用的OpenMP命令进行分析。
2. 概述
OpenMP是一种用于共享内存并行系统的多线程程序设计方案,支持的编程语言包括C、C++和Fortran。OpenMP提供了对并行算法的高层抽象描述,特别适合在多核CPU机器上的并行程序设计。编译器根据程序中添加的pragma指令,自动将程序并行处理,使用OpenMP降低了并行编程的难度和复杂度。当编译器不支持OpenMP时,程序会退化成普通(串行)程序。程序中已有的OpenMP指令不会影响程序的正常编译运行。
OpenMP执行模式
OpenMP采用fork-join的执行模式。开始的时候只存在一个主线程,当需要进行并行计算的时候,派生出若干个分支线程来执行并行任务。当并行代码执行完成之后,分支线程会合,并把控制流程交给单独的主线程。
一个典型的fork-join执行模型的示意图如下:
OpenMP编程模型以线程为基础,通过编译制导指令制导并行化,有三种编程要素可以实现并行化控制,他们分别是编译命令、API函数集和环境变量。
3. 编译命令
编译制导指令以#pragma omp 开始,后边跟具体的功能指令,格式如:#pragma omp 指令[子句[,子句] …]。
1. #pragma omp parallel
以此命令开头的代码块被标识为并行区,一般并行区都要以该命令开头。
#pragma omp parallel
{
block;
}
2. #pragma omp for
后面必须跟for循环,将循环分配给不同线程。
#pragma omp parallel
#pragma omp for
for(...){
block;
}
这里也可以将两个宏合并成一个,使得代码更简洁
#pragma omp parallel for
for(...){
block;
}
3. private or shared
OpenMP默认将全局变量,静态变量设置为共享属性,但是可以通过下列命令控制:
shared(varname, …) : 设置变量varname为共享
private(varname, …) : 设置变量varname为线程私有
#pragma omp parallel for private(x, y)
#pragma omp parallel for shared(sum)
4. critical
设置临界区, 使得同一时刻只能有一个线程访问函数。
#pragma omp critical
sum += a[i] +b[i]
return sum
也可以给这个命令添加参数,将同一函数放在不同临界区中,使同一时刻可以有多个线程访问。
#pragma omp critical(R1_lock)
consum(B, &R1)
#pragma omp critical(R2_lock)
consum(A, &R2)
5. reduction
归约操作,reduction(op:list),op表示一个操作,list代表执行op操作的变量列表。
每个线程会各自拥有一个私有化的list中的变量,当所有线程计算完成后,对各个线程的私有化list进行op操作。
#pragma omp parallel for reduction(+:sum)
for(i=0; i<n; i++){
sum += a[i] + b[i];
}
6. schedule
这个命令用于控制工作量的划分与调度。
//静态划分,将for循环的每chunk次循环划分为一个块,依次将一块分配给一个线程
schedule(static[,chunk])
//动态划分,将chunk次循环划分成一个块,但工作块被放入队列,线程完成一块后从队列中
//取下一个新块,即线程执行哪一块是不确定的。
schedule(dynamic[,chunk])
//动态划分的改进,但分块的大小是不固定的,一开始块比较大,随着工作量的减少,
//块的大小也随之减小,chunk规定块的最小值
schedule(guided[,chunk])
7. section
可以定义多个section,然后让这些section并行地执行,每个section的工作之间应该是相互独立、没有依赖关系的,否则并行的效果不好。
#pragma omp parallel sections
{
#pragma omp section
block(1);
#pragma omp section
block(2);
#pragma omp section
block(3);
}
8. single
这个命令后面的代码只执行一次,即只有一个线程会去执行它。
#pragma omp parallel
{
DoManyThings();
#pragma omp single
{
ExchangeBounderies();
}
DoManyMoreThings();
}
9. master
master与single的作用类似,但不同之处在于master不会阻碍其他线程的继续运行,即其他线程如果遇到#pragma omp master会直接跳过去执行后面的语句。
#pragma omp parallel
{
DoManyThings();
#pragma omp master
{
ExchangeBounderies();
}
DoManyMoreThings();
}
10. nowait
parallel, for, single, sections等都包含隐藏的障碍,即当一个线程完成了并发区的工作之后,为了保证同步,必须等到其他所有线程都结束才能继续运行。
但是如果后面的代码对并行区是没有依赖的,那么这样的等待就是在浪费时间,这时就可以用nowait来去除障碍。
#pragma omp parallel for nowait
for(...){
...
}
11. barrier
设置一个阻塞,所有线程都执行到该行后,所有线程才能继续执行后面的代码。
#pragma omp barrier
4. API函数集
头文件
Faiss已经添加了对OpenMP的支持,因此只要在代码中添加头文件omp.h即可。
#include <omp.h>
omp_get_thread_num
返回当前线程的ID, 0~(N-1),N为并行区总线程数。
int omp_get_thread_num(void);
omp_get_num_threads
返回并行区里的总线程数
int omp_get_num_threads(void)
5. 环境变量
OpenMP中定义一些环境变量,可以通过这些环境变量控制OpenMP程序的行为,常用的环境变量:
OMP_SCHEDULE
用于for循环并行化后的调度,它的值就是循环调度的类型;
OMP_NUM_THREADS
用于设置并行域中的线程数;
OMP_DYNAMIC
通过设定变量值,来确定是否允许动态设定并行域内的线程数;
OMP_NESTED
指出是否可以并行嵌套。
OMP_WAIT_POLICY
设置正在等待的线程的所需策略:ACTIVE 或 PASSIVE。
ACTIVE 线程在等待时会占用处理器时间。PASSIVE 线程不会占用处理器时间,并且可能会放弃处理器或进入休眠状态。