OpenMP 快速入门

news2024/11/26 18:47:23

学习《高性能计算:现代系统与应用实践》(Thomas Sterling,Matthew Anderson,Maciej Brodowicz)第 7 章 OpenMP 的基础

OpenMP

  • OpenMP 是一个 API
    • C、C++、Fortran
  • OpenMP 是共享内存多线程编程模型
    • 共享内存
      • 默认所有线程可以直接访问全局变量
      • 可以限制变量为各线程私有(e.g. 索引变量 i
    • 线程并行
      • master/worker 线程:fork-join 模型
      • 分支:SPMD(单程序多数据)
      • 聚合:隐式栏栅同步:超出当前访问前,所有线程都要完成

openmp-fork-join

环境变量

export OMP_NUM_THREADS=8  # 开几个线程,默认自动看 CPU
export OMP_DYNAMIC=TRUE   # 动态线程数量
export OMP_NESTED=TRUE    # 允许嵌套并行
export OMP_SCHEDULE=schedule.chunk  # 循环的负载分布

使用 OpenMP

不用安装:

  • GCC 内置了 OpenMP 支持!
  • 编译的时候带上 -fopenmp 就行。

导入:

#include <omp.h>

基本函数:

omp_get_num_threads()  // 有多少个并行的进程: OMP_NUM_THREADS
omp_get_thread_num()   // 我(当前进程)的 id
                       // master: 0; worker: [1, OMP_NUM_THREADS)

OpenMP 指令(制导语句):

#pragma omp <directive> <clauses> <statement>

Hello World

// hello.c

#include <omp.h>
#include <stdio.h>

int main() {
#pragma omp parallel
    {  // fork
        int num_threads = omp_get_num_threads();
        int thread_id = omp_get_thread_num();
        printf("Hello world from %d (total %d)\n", thread_id, num_threads);
    }  // join

    return 0;
}

魔法是 #pragma omp parallel:串行 -> 并行。派生出 OMP_NUM_THREADS 个进程来并行跑其后的一个代码块。

编译运行:

$ gcc-11 -fopenmp hello.c
$ ./a.out
Hello world from 1 (total 4)
Hello world from 0 (total 4)
Hello world from 3 (total 4)
Hello world from 2 (total 4)

顺序是那种乱七八糟的:并行。觉得不明显可以指定线程数量,再运行:

$ OMP_NUM_THREADS=100 ./a.out
Hello world from 2 (total 100)
Hello world from 11 (total 100)
...
Hello world from 35 (total 100)
...
Hello world from 0 (total 100)
Hello world from 98 (total 100)

私有变量

上面那个程序的另一种版本:

  • 预先定义两个变量,但是指定为各线程私有
  • 只让 id 为 0 的进程,即主线程获取 num_threads
  • (其实我试了,编译成汇编和上面那个区别不大,两个变量结束并行之后都不是有效值。所以目前还不清楚这样在外面先定义好变量有啥优势 这个问题大致有答案了,见后文:[[#Why private]]。我还是喜欢局部变量就局部去定义。) ^b5e08c
// hello-v2.c

#include <omp.h>
#include <stdio.h>

int main() {
    int num_threads, thread_id;

#pragma omp parallel private(num_threads, thread_id)
    {
        thread_id = omp_get_thread_num();
        printf("Hello world from thread %d.\n", thread_id);

        if (thread_id == 0) {
            num_threads = omp_get_num_threads();
            printf("Total number of thread is: %d\n", num_threads);
        }
    }

    // printf("End of parallel: %d, %d\n", thread_id, num_threads);
    // End of parallel: 1, 61694048

	return 0;
}

编译运行:

$ gcc-11 -fopenmp hello-v2.c; ./a.out 
Hello world from thread 0.
Total number of thread is: 4
Hello world from thread 3.
Hello world from thread 2.
Hello world from thread 1.

小结:OpenMP 并行:

#include <omp.h>

int main() {
    #pragma omp parallel
    {
        并行的代码;
    }
}
  • 默认各线程共享上下文中的变量:在 omp parallel 后面加 private(...) 指定要各线程私有的变量。

OpenMP 并行

并行 for

OpenMP parallel for

线程间循环分布:通过 OpenMP,让多个线程同时(并行)处理一个 for 循环。

#include <stdio.h>
#include <stdlib.h>

int main() {
    const int N = 20;
    int num_threads, thread_id;

    double a[N], b[N], result[N];
    for (int i = 0; i < N; i++) {
        a[i] = 1.0 * i;
        b[i] = 100.0 * i;
    }

    for (int i = 0; i < N; i++) {
        result[i] = a[i] + b[i];
    }

    printf("TEST result[19]=%g\n", result[19]);
    // TEST result[19]=1919
    return 0;
}

要并行化处理合并两个数组的操作,只需加 5 行:

#include <omp.h>  // +1

int main() {
    ...

    #pragma omp parallel  // +2
    {  // +3
        #pragma omp for  // +4
        for (int i = 0; i < N; i++) {
            printf("%d: i=%d\n", omp_get_thread_num(), i);
            result[i] = a[i] + b[i];
        }
    }  // +5
    
    printf("TEST result[19]=%g\n", result[19]);
    return 0;
}

此处的魔法是 #pragma omp for,把 for 分配给各个线程。中间额外加了一个 printf,可以看到 20 次循环,被几个线程平分:

0: i=0  ... 0: i=4
2: i=10 ... 2: i=14
3: i=15 ... 3: i=19
1: i=5  ... 1: i=9
TEST result[19]=1919

如果露掉这一行 #pragma omp for,OpenMP 便无从得知你要并行这个 for。那就比较“精彩”了,4 个线程,把循环各跑一遍,极致反向优化:

0: i=0 ... 0: i=19
1: i=0 ... 1: i=19
2: i=0 ... 2: i=19
3: i=0 ... 3: i=19
TEST result[19]=1919

这个操作太常用了,所以有简化的写法:

#pragma omp parallel for
for (int i = 0; i < N; i++) {
    printf("%d: i=%d\n", omp_get_thread_num(), i);
    result[i] = a[i] + b[i];
}

小结:并行 for:

#pragma omp parallel
{
    #pragma omp for
    for (;;) {}
}

⬇️ 复合魔法

#pragma omp parallel for
for (;;) {}
  • 可选:omp for 后面加子句 schedule(static.chunk),指定切分循环的方式。

并行 sections

OpenMP parallel sessions

有多块代码要并发执行:

  • 一个块称为一个 section
  • 多个 section 在一个 sections 中并行
  • 一个线程处理一个 section
#pragma omp parallel
{
    #pragma omp sections
    {
        { /* section 0 */ }

        #pragma omp section
        { /* section 1 */ }

        #pragma omp section
        { /* section 2 */ }

        ...
    }
}

类似于 parallel for,如果开并行只是为了执行 sections,也可以用合并的 parallel sections

#pragma omp parallel sections
{
    { /* section 0 */ }

    #pragma omp section
    { /* section 1 */ }

    ...
}

一个例子:并行地对一列数据进行统计,求最值、均值、方差:

#include <omp.h>
#include <stdio.h>
#include <stdlib.h>

int main() {
    const int N = (1<<29);  // N doubles: 4 GiB
    // const int N = 10;
    double *x = calloc(N, sizeof(double));
    // #pragma omp parallel for  // 初始化
    for (int i = 0; i < N; i++) {
        x[i] = i;
    }

    // 总和、均值、平方的总和
    double sum = 0.0, avg = 0.0, sum2 = 0.0;

    #pragma omp parallel sections shared(x, sum, avg, sum2)
    {
        // section 0: 计算 最大最小值
        {
            double max = (1<<31), min = (1<<31) - 1;
            for (int i = 0; i < N; i++) {
                if (x[i] < min) min = x[i];
                if (x[i] > max) max = x[i];
            }
            printf("min: %f\nmax: %f\n", min, max);
        }

        #pragma omp section  // section 1: 计算总和、均值
        {
            for (int i = 0; i < N; i++) {
                sum += x[i];
            }
            printf("sum: %f\n", sum);

            avg = sum / N;
            printf("avg: %f\n", avg);
        }

        #pragma omp section  // section 2: 计算平方的均值
        {
            for (int i = 0; i < N; i++) {
                sum2 += x[i] * x[i];
            }
            printf("sum2: %f\n", sum2);
        }
    }

    // 方差 = 平方的均值 - 均值的平方
    double var = sum2 / N - avg * avg;
    printf("var: %f\n", var);

    return 0;
}

注意这里使用了全局共享的 sum 等几个量,是为了在并行结束后,留下这些值,用于计算方差。

编译运行,对比去掉 parallel 的版本,似乎有一定的提升:

$ openmp gcc-11 -fopenmp sections.c; time ./a.out    
min: 0.000000
max: 536870911.000000
sum2: 51580834826121141939077120.000000
sum: 144115187606093856.000000
avg: 268435455.125000
var: 24019198213991840.000000
./a.out  7.34s user 4.00s system 101% cpu 11.200 total

$ openmp gcc-11 -fopenmp no-sections.c; time ./a.out    
min: 0.000000
max: 536870911.000000
sum: 144115187606093856.000000
avg: 268435455.125000
sum2: 51580834826121141939077120.000000
var: 24019198213991840.000000
./a.out  8.41s user 9.52s system 72% cpu 24.587 total

OpenMP 同步

共享内存:

  • OpenMP 的多个并发线程之间共享全局数据
  • 无需 send/recv 的消息传递在并发进程之间交换数值

同步机制:

  • 协调并行程序中多个并行线程的执行
  • 控制顺序:避免竞争 -> 冲突
  • 隐式:join 栅栏
  • 显示:critical、master、barrier、single

critical 指令

临界同步指令 critical:多个并行线程互斥访问共享变量。

#paragma omp critical
{ ... }

e.g. 尝试做个并行计数器:

#include <omp.h>
#include <stdio.h>

int main() {
    int n = 0;

    #pragma omp parallel for shared(n)
    for (int i=0; i < 40000; i++) {
        #pragma omp critical
        n = n + 1;
    }

    printf("n: %d\n", n);
    return 0;
}

编译运行(运行了很多次都是对的):

$ openmp gcc-11 -fopenmp critical.c; time  OMP_NUM_THREADS=1000 ./a.out    
n: 40000
OMP_NUM_THREADS=1000 ./a.out  0.01s user 0.05s system 16% cpu 0.390 total

如果删掉 #pragma omp critical,就会出现喜闻乐见的错误结果(多运行一些次就能看到各种不同的错误结果):

$ openmp gcc-11 -fopenmp no-critical.c; time  OMP_NUM_THREADS=1000 ./a.out    
n: 39960
OMP_NUM_THREADS=1000 ./a.out  0.01s user 0.06s system 63% cpu 0.104 total

master 指令

master 指令:只有主线程执行这一块代码,其他线程遇到则跳过。

  • 主线程:执行这一块代码
  • 其他线程:直接往下走,不等
#pragma omp master
{ ... }

barrier 指令

barrier 指令:同步所有并发线程:

  • 遇到 barrier 的线程就停下来,等;
  • 等所有进程都到了 barrier 才能继续。
#pragma omp barrier

single 指令

single 指令:宽松版 master + 隐式 barrier

  • 在代码块({ ... }后面放一个隐式 barrier;
  • 允许任意线程 Foo 执行代码块;
  • 其他线程跳过代码块执行,但是阻塞在 barrier,等 Foo 酱执行完代码块再放行。
#pragma omp single
{ ... }

reduction 指令

规约:将大量值组合在一起,生成单个结果值。

Reduction:the action or fact of making a specified thing smaller or less in amount, degree, or size
—— New Oxford American Dictionary

这里所谓规约就是让值的个数变少的操作。(回想一下 Lisp 就很形象了。)

OpenMP 可以用 reduction 指令做规约:

double result;

#pragma omp reduction(op : result)
{
    result = ...;  // 局部 result
}

// 全局 result: result = reduce(op, results)
  • op 为某种运算:+-*/&|^ 中的一个;
    • 更复杂的规约也容易使用其他同步的方式实现。
  • result 为结果变量,注意这个值在块内是各线程私有,出来之后变成全局的、规约得到的结果。

OpenMP reduction

e.g.

#include <stdio.h>
#include <omp.h>

int main() {
    const int N = 16;
    float a[N], b[N], result;

    for (int i = 0; i < N; i++) {
        a[i] = i * 1.0;
        b[i] = i * 2.0;
    }

    #pragma omp parallel for reduction(+ : result)
    for (int i = 0; i < N; i++) {
        result += a[i] * b[i];
    }

    printf("Result = %f\n", result);
    
    return 0;
}

编译运行:

$ gcc-11 -fopenmp reduction.c; ./a.out    
Result = 2480.000000

闲聊

Why pragma

#pragma 是什么鬼啊?pragma 来源于 pragmatic(务实的),,所以关联的点是什么。。

Pragma,也叫做 directive。Directive 就是“指示”的意思,不知到是如何惨遭毒手,被翻译为“制导语句”的。。不翻译成“42 号混凝土”我是不敢苟同的。

总之这东西就是用来指导编译器如何编译的。(教编译器做事)

(对了,我最讨厌这种要左手连打几个字母的词了,顺便吐槽一下“Database”,这个词简直了,尤其是要大写时。其实换一种角度来考虑,还是键盘键位设计的锅。)

Why private

private 子句有何用?[[#^b5e08c|前文]]提到,在各线程内定义局部变量可以完全避免 private,但这是站在一开始就编写 OpenMP 并行代码的角度来设计得到的结果。

OpenMP 设计的一项初衷是在尽可能少改动原有串行代码的基础上,加入并行支持。理想的情况只需加入尽可能少的制导语句(#pragma omp parallel#pragma omp parallel for 等)就可以让原本串行的代码变并行。

所以就有这种场景:原本的代码如下:

int i, j;
for(i = 0; i < n; i++) {
      for(j = 0; j < n; j++) {
          //do something
      }
}

要使其支持并发,有 private 的支持,只需加一行制导语句,完全不需要改动原有任何一行代码:

int i, j;
#pragma omp parallel for private(j)
for(i = 0; i < n; i++) {
    for(j = 0; j < n; j++) {
        //do something
    }
}

但如果没有 private,就需要改动原有代码结构,把 j 的定义移动到第一层循环内:

int i;
#pragma omp parallel for
for(i = 0; i < n; i++) {
    int j;
    for(j = 0; j < n; j++) {
        //do something
    }
}

如果需要为 C89 (所有变量定义要写到 scope 顶部)写的代码加入并行性,这个 private 对于还是比较有意义的。

所以更多是一种兼容性吧。


最近学这些计算(有没有一种可能计算机本来就是用来做计算的😭),还真有好多代码是八几年、九几年写的,有些甚至写的是 K&R C,一直沿用至今,正确、高效、优雅、美观,真的 nb(褒义,由衷赞叹,写的确实漂亮,以带着最深沉偏见的、最批判的锋利目光去看,也无可挑剔)。

再看看现在好多所谓学“*”的 * 写的代码,我没有针对 * 语言,我是说 * 写的任何程序,真的 nb(贬义。那些“代码”,笑死,看别人接手是喜剧,要自己接手是悲剧;眼睁睁看着别人写出这种代码是人间炼狱)。

  • StackOverflow: OpenMP: are local variables automatically private?
  • StackOverflow: Is there any difference between variables in a private clause and variables defined within a parallel region in OpenMP?
  • StackOverflow: Variable declaration placement in C

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/50189.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

22年11月-自研-面试题

目录背景题目Activiti回退功能条件分支功能&#xff0c;并行网关、包含网关有没有用到流程流转中&#xff0c;需知会其他人&#xff0c;这些人需同意/做处理&#xff08;有点流程的感觉&#xff09;&#xff0c;最后所有的意见都要汇总。你的实现思路Redis哪些数据结构&#xf…

STM32实战总结:HAL之低功耗

低功耗的含义不必过多解释&#xff0c;一听就能懂。 低功耗对电池供电产品尤其重要。 STM32的有三种低功耗模式&#xff0c;即睡眠模式、停止模式和待机模式。 在我的印象中&#xff0c;停止不就是关机吗&#xff1f;但并不是。 在系统或电源复位以后&#xff0c;微控制器处于运…

基于最小二乘插值(Least-Squares Interpolation)图像超分辨率重构算法研究-附Matlab代码

⭕⭕ 目 录 ⭕⭕✳️ 一、引言✳️ 二、最小二乘图像插值理论与Matlab处理✳️ 三、基于最小二乘插值超分辨重构实验验证✳️ 四、参考文献✳️ 五、Matlab程序获取与验证✳️ 一、引言 图像超分辨率重构技术起源于上世纪60年代Harris和Goodman构造的单帧图像重构方法&#xf…

AutoCAD Electrical 2022—元件的绘制

原理图——图标菜单 选择要插入的元件&#xff1b; 根据实际情况&#xff0c;选择垂直放置还是水平放置&#xff0c;比例大小&#xff1b; 选择一个三极断路器&#xff0c;垂直放置&#xff1b; 点击确定后&#xff0c;点击一根导线&#xff0c;选择断路器另外两个符号是的方向…

相控阵天线(十一):阵列天线有源驻波分析

目录简介有源驻波概念和计算公式平面阵列天线的有源驻波平面阵列有源驻波计算公式平面阵列有源驻波仿真示例不同耦合系数/隔离度的有源驻波分析简介 有源相控阵最大的特点是每一个收发天线后均连接一个独立的T/R组件&#xff0c;每一个T/R组件相当于一个常规雷达的高频前端&am…

【信息检索与数据挖掘期末笔记】(二) IR Evaluation

文章目录测试集无序检索结果集合的评价Precision & RecallAccuarcy?F值有序检索结果评价方法二值相关&#xff08;相关/不相关&#xff09;PrecisionK&#xff08;PK&#xff09;Mean Average Precision&#xff08;MAP&#xff09;Mean Reciprocal Rank多级相关CG&#x…

LeetCode542. 01 矩阵(C++中等题)

题目 给定一个由 0 和 1 组成的矩阵 mat &#xff0c;请输出一个大小相同的矩阵&#xff0c;其中每一个格子是 mat 中对应位置元素到最近的 0 的距离。 两个相邻元素间的距离为 1 。 示例 1&#xff1a; 输入&#xff1a;mat [[0,0,0],[0,1,0],[0,0,0]] 输出&#xff1a;[[…

(三) 共享模型之管程【共享带来的问题】

一、共享带来的问题 1. 临界区 &#xff08;1&#xff09;一个程序运行多个线程本身是没有问题的 &#xff08;2&#xff09;问题出在多个线程访问共享资源 1️⃣多个线程读共享资源其实也没有问题 2️⃣在多个线程对共享资源读写操作时发送指令交错&#xff0c;就会出现问题 …

git@github.com: Permission denied (publickey).

本地虚拟机ubuntu上安装git&#xff0c;想从github上拉取项目到ubuntu上的过程。 1、在ubuntu上安装git 更新apt指令 sudo apt update 安装git sudo apt install git 查看安装git版本 git --version 2、ssh认证 首先已经安装了ssh指令 先执行 ssh -T gitgithub.com 执行之…

3.11 怎么增加小红书评论区的互动?【玩赚小红书】

今天就为大家总结了一下&#xff0c;关于小红书粉丝互动的一些小技巧&#xff0c;来供大家参考。 ​ ​ 一、 固好“真爱粉” 经常会在笔记下面评论、点赞、浏览笔记内容的粉丝&#xff0c;也就是所谓的“真爱粉”、“铁粉”&#xff0c;我们就需要用心维护这一部分粉丝。 ​…

虹科分享|硬件加密U盘|居家办公的网络安全:远程员工可以采取的步骤

新冠肺炎的流行迫使数以百万计的人在家工作&#xff0c;而当时他们对这一概念知之甚少&#xff0c;甚至完全没有经验。虽然许多员工已经重返办公室&#xff0c;但最近的一项研究发现&#xff0c;72%的受访者希望每周至少有两天在家工作&#xff0c;32%的人表示他们希望永久在家…

全波形反演的深度学习方法: 第 4 章 基于正演的 FWI (草稿)

本章论述经典的 FWI, 它基于正演方法. 本贴仅供内部培训. 4.1 FWI 问题 图 4.1 FWI 的输入与输出 [1].图 4.2 FWI 的数学式子.正演问题是建立从速度模型到地震数据的映射. 一般认为是单解的, 即一个速度模型只能生成一个地震数据 (如果不考虑噪声).反演问题是建立从地震数据到…

【题解】E. Sending a Sequence Over the Network(1741)

链接&#xff1a;https://codeforces.com/problemset/problem/1741/E 题目大意 给出一个数组&#xff0c;判断它是否是合法的&#xff0c;如果合法则输出YES&#xff0c;不合法则输出NO。 合法规则&#xff1a;一段序列中&#xff0c;这个序列的第一个或者最后一个的数值&…

岩藻多糖-聚乙二醇-胆固醇Cholesterol-PEG-FucoidanFucoidan-Cholesterol 岩藻多糖-胆固醇

岩藻多糖-聚乙二醇-胆固醇Cholesterol-PEG-FucoidanFucoidan-Cholesterol 岩藻多糖-胆固醇 中文名称&#xff1a;岩藻多糖-胆固醇 英文名称&#xff1a;Fucoidan-Cholesterol 别称&#xff1a;胆固醇修饰岩藻多糖&#xff0c;胆固醇-岩藻多糖 外观:固体或粘性液体&#xff…

终于有人将TWI(串行通讯接口)给讲通了!

目录 TWI的特性 数据传输格式 时钟同步 数据仲裁 功能描述 总线接口单元 频率生成单元 地址匹配单元 控制单元 传输模式 主机发送模式 主机接收模式 从机发送模式 从机接收模式 TWI的特性 两线模式&#xff0c;简单快捷&#xff1b;支持主机模式和从机模式&#xff…

「科普」如何评价供应商的MES系统

MES综合性很强&#xff0c;涉及到多个业务领域、多种技术和多专业&#xff0c;如何写好最难的投标技术方案呢&#xff1f;简搭(jabdp)根据多年经验&#xff0c;为大家进行梳理和分解&#xff0c;帮助发愁的你写出好方案&#xff01; MES是一个综合性很强的系统&#xff1a; 生…

68 - 令人迷惑的写法

---- 整理自狄泰软件唐佐林老师课程 1. 写法一 下面的程序想要表达什么意思&#xff1f; 1.1 历史原因 早期的C直接复用class关键字来定义模板 但是泛型编程针对的不只是类类型 class关键字的复用使得代码出现二义性 1.2 typename诞生的直接诱因 自定义类类型内部的嵌套…

猿如意|手把手教你下载、安装和配置PyCharm社区版

手把手教你使用猿如意下载、安装和配置PyCharm社区版&#xff0c;希望能帮助到有需要的童鞋。 文章目录前言一、下载安装猿如意二、安装PyCharm社区版1.通过猿如意找到PyCharm下载位置2.安装PyCharm三、对PyCharm社区版进行简单设置1.设置PyCharm社区版为中文2.安装第三方Pytho…

数据同步,还看Canal

一个系统最重要的是数据&#xff0c;有时对于一个业务场景&#xff0c;不单单是把数据保存在数据库中&#xff0c;还需要同步保存在ES&#xff0c;Redis等等中。这时阿里开源组件Canal由此而生&#xff0c;它可以同步数据库中的增量数据保存到其它存储应用中。 一、介绍 canal…

航空专场 | 无人机设计仿真流程讲解与案例实操

一、CFD在无人机上的应用 1、静、动气动系数计算以上介绍的无人机的流动状态一般为中低雷诺数&#xff0c;不可压缩流动。这些计算一般用S-A模型或者KW-SST模型进行计算&#xff0c;能够获得不错的工程精度。静、动气动力系数主要用于无人机操纵性和稳定性的分析&#xff0c;评…