学习openmp-自定义reduction

简介

本文介绍openmp中reduction的进阶用法,针对于非内置数据类型的自定义reducion用法。从上一篇文章【学习openmp-reducion】已经了解了reduction并行优化处理,其中内容都是openmp-reduction的基础用法,针对c++原有的内置数据类型,适用于int、float等内置的数据类型并行reduction处理,对于自定义的数据类型并不适用。针对于此问题,openmp推出了自定义的数据类型的reduction操作(custom-reduction)。需要注意的是此功能在OpenMP 3.x以上版本才支持,所以在windows的mvsc编程环境下是不适用的。

语法

自定义的reduction操作实际上是openmp提供让用户自行声明reduction操作符,原本默认的reduction操作如+、-、min与max等openmp内置的reduction操作符是针对与c++缺省变量类型int、float等。对于自定义类型(比如一个struct或者class),+、-、min与max等操作就不再适用,需要自定义操作符并实现其具体的reduction操作,有一点c++的重载操作符或者重载函数的意思。自定义一个操作符的语法如下:

1
#pragma omp declare reduction (reduction-identifier : typename-list : combiner) [initializer-clause]

说明:

  • reduction-identifier:归约标识符,相当于openmp自带的+,这里命名为MyAdd
  • typename-list: 归约操作的数据类型,这里为MyClass
  • combiner: 合并链接具体操作,+=为具体操作,omp_out与omp_in为固定的标识符
  • initializer-clause: 归约操作的每个线程的初始值,比如求和操作时赋值100则等效于100xn(线程数)基础上再求和数组,定义格式为initializer(omp_priv=MyClass(100)) ,此项可以省略不写,初值会按类型的默认构造函数赋值

从上诉解释去看openmp内置的+-,min,max内置操作符,依据上述语法,其实是这么定义的

1
2
3
#pragma omp declare reduction (+ : T : omp_out += omp_in) omp_priv = 0
#pragma omp declare reduction (min : T : omp_out = std::min(omp_out,omp_in)) omp_priv = std::numeric_limits<T>::max()
//..

当然内部实际情况未必是这么实现的,我这里只是从语法规则角度去解析内置的reduction在语法规则下可以这么定义。

具体例子

接下来看具体例子,假如有以下这么一个自定义的数据类型:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
struct MyClass
{
    int data;
    MyClass(const int& data_): data(data_){}
    MyClass& operator = (const MyClass& other)
    {  
        return *this;
    }
    MyClass& operator += (const MyClass& other)
    {
        data += other.data;
        return *this;
    }
};

需要计算多个该类型数据的某个成员的总和时,openmp并行优化可以这么实现,首先需要定义该数据类型求和+=的openmp-reduction操作,依据上文所述的语法,MyClass的成员数据data+=操作可以定义为

1
2
3
4
#pragma omp declare reduction(MyAdd: MyClass: omp_out += omp_in) initializer(omp_priv=MyClass(0)) 

//初值赋予100, 与MyAdd区别就是每个线程的结果会多100,总结果会多出nx100,n为并行的线程数
#pragma omp declare reduction(MyAdd_with100: MyClass: omp_out += omp_in) initializer(omp_priv=MyClass(100)) 

定义好后,for循环优化使用方法:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
  MyClass sum(0);
    std::vector<MyClass> vec(100, 1);
    
#pragma omp parallel for reduction(MyAdd: sum)
    for(int i = 0; i < vec.size(); ++i)
    {
        sum += vec[i];
    }
    std::cout << "custom reduction sum = " <<sum.data << std::endl;
    sum.data = 0;
#pragma omp parallel for reduction(MyAdd_with100: sum)
    for(int i = 0; i < vec.size(); ++i)
    {
        sum += vec[i];
        std::cout << "initializer value = 100,  threads count = "<< omp_get_num_threads() << std::endl;
    }

在4线程的处理器平台运行下,结果为:

1
2
3
4
5
custom reduction sum = 100
initializer value = 100,  threads count = 4
...
initializer value = 100,  threads count = 4
custom reduction with 100  initializer sum = 500

两者相差400,符合预计的结果情况。接下来看另外一个例子,展示custom-reduction功能不仅可以实现简单的加减求和或者积指的归约,发挥好语法规则,还可以实现多个数组合并的操作。首先先看定义:

1
#pragma omp declare reduction(MyMerge: std::vector<MyClass>: omp_out.insert(omp_out.end(), omp_in.begin(), omp_in.end()))

注意看

1
omp_out.insert(omp_out.end(), omp_in.begin()

这段combiner语法的区别,这里近看代码就是将omp_in数组合并到omp_out的操作,实际上就是openmp最后多个线程结果出来后,其中一个线程(omp_in)结果合并到另一个线程(omp_out)中去,这个合并操作不仅限于加减乘除等简单运算,一般来说符合c++语法的都可以,掌握这一点就就可以灵活写出复杂类型的openmp-reduction并行优化。最后再看具体使用:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
    std::vector<MyClass> merge;
    std::vector<std::vector<MyClass>> list(100, std::vector<MyClass>(5, 1));

#pragma omp parallel for reduction(MyMerge: merge)
    for(int i = 0; i < list.size(); ++i)
    {
        merge.insert(merge.end(), list[i].begin(), list[i].end());
    }

    std::cout << "merge size = "<< merge.size() << std::endl;

执行后结果为:

1
custom reduction with 100  initializer sum = 500

与理论预计结果相符合。

本文练习代码已上传至github:https://github.com/mangosroom/learn-openmp/tree/main/custom_reduction


本文由芒果浩明发布,转载请注明出处。 本文链接:https://mangoroom.cn/parallel-programming/learn-openmp-custom-reduction.html