OpenMP 中 static 和 dynamic schedule 方式的区别

最近有在做一些跟多线程相关的事情,不可避免的就会接触到 OpenMP ,但是各种文章对其中的调度器并没有过多的研究。因此在收集各方资料之后,我终于弄明白了动态调度和静态调度的区别,在这里跟大家分享一下。

基本解释

  • static :平均的分配任务到所有线程
  • dynamic:一个线程做完了之后会再去拿一个新的任务来做,因此有 load balance 的作用
  • chunksize(n):对于 for 循环来说,将 n 个循环绑在一起分配给线程

调度器是如何做到任务分配的

Static 调度

  • 基本的原理是首先将迭代空间按照 chunksize 进行划分,分成好几个 chunk,然后一个一个的分给各个线程

领域展开!上代码!

  • 算法的主要原理是两层循环
    • 第一层循环通过线程id,将不同的 chunk 按照余数分配给不同的线程
    • 第二层才是chunk本身内部的循环,执行 loop body
  • 因此在编译器时期就能确定什么线程执行哪些循环了,没有太大的 overhead

Dynamic 调度

  • 基本的原理依然是首先分成好几个 chunk,区别在于这次并不是直接分,而是先分给每个线程一个,做完了再来拿 领域展开!上代码!

  • 依然是两层循环,本质上没有区别,只是将使用步长计算出来的 chunk 变成了动态获取的 chunk,依赖于 ort_get_dynaic_chunk 函数,内层还是执行 for 循环的循环体。

  • 可以继续看一下 ort_get_dynamic_chunk 的代码实现

  • 动态调度依赖于一个数据结构记录当前迭代空间执行到哪里了,下方的变量 t 呆滞数据结构 gdopt_t

    • data 保存当前第几个迭代还未分配
    • lock 是一个锁,所有线程如果要获取新的迭代来执行就要先获取锁
      • (还不确定是一个什么 lock,书里面说是 mutex lock,但是就改一点点数据感觉肯定是 spin lock 把。。。。)
  • 在实现上可以看出,每次就是先获取 lock,再更新一下 data 指向的循环偏移量。因此,动态调度的overhead 在于每次获取新的 chunk 的时候都会有锁争用,因此如果chunksize 比较小或者线程数比较多可能就会影响性能

  • 结果上的区别:

    • 静态的 scheduler 会在编译时期将 workload 分给不同的线程
    • 动态的 scheduler 会在程序执行时期将 workload 分给不同的线程

如何进行 schedule 方式的选择(理论上)

  • 将任务并行化需要满足三条原则

    1. 分解代价低:也就是获取不同的 chunk 的时候的代价,这个代价主要来自于两部分
      1. 创建线程
      2. schedule 方式引入的 overhead
    2. 在不同的线程中计算量要均衡,否则会因为木桶效应导致执行最长的线程影响整个任务的执行时间,影响这个指标的原因有:
      1. 每个线程获得的 chunk 数量是否一致(是否能刚好均分)
      2. loop body 中的计算量在每个循环中是否一致,比如第一个循环执行一个加法,第100个循环执行 100 个加法
      3. 计算能力的差异,比如 CPU P core 和 E core,以及内存的 NUMA节点等
    3. 避免 cache 冲突:(具体可以看多线程的 false sharing 问题)这个跟 scheduler 关系不大,主要看 chunksize 的选择
  • 而对于两种 schedule 方式,总结如下:

    static dynamic
    分解代价 几乎无 overhead,依赖简单的加法运算得到 一个自旋锁,会有锁争抢的问题
    计算量均衡 会因为循环体计算量的不同或者计算能力的差异而出现木桶效应,一些早做完的线程在等待没有做完的线程 提早执行完的线程可以去获取其他的任务,因此计算量均衡方面处理的很好
    cache 冲突 依赖于 chunksize 同样依赖于 chunksize
  • 所以,如何进行选择呢?答案是默认直接使用 dynamic,以opencv 图像处理库为例,他的 openmp 就是直接用的 dynamic 调度。

具体场景考虑

场景1:CPU 有 P core E core 这种混合架构

  • 不要用 static
  • 同样的任务,E core 的执行时间要长于 P core,static 会出现任务分配不均衡的问题,导致并行甚至不如单线程

场景2:线程数特别多

  • 不要用 dynamic
  • 线程数多就会导致大量的锁争用,导致效率降低,这个情况在比如服务器这种 CPU 核数很高的场景下需要注意

场景3:循环迭代次数很少,比如迭代数等于线程数

  • 选哪个都无所谓,因为 dynamic 会退化为 static
  • 每个线程还是执行1~2个循环体,连获取 chunk 的机会都没有,schedule 当然没有意义了,这种情况就要提示你应该改改你的代码了

一些学习资料:

  1. OpenMP 视频教程:www.youtube.com/watch?v=nE-...
  2. 《OpenMP编译原理及实现技术》:本文代码和图片取自该书
相关推荐
C#Thread17 分钟前
机器视觉--Halcon的数据结构(数组)
算法
你在我身后1 小时前
Spring-JAVA
java·后端·spring
垠二2 小时前
L2-4 寻宝图
数据结构·算法
东方芷兰4 小时前
算法笔记 04 —— 算法初步(下)
c++·笔记·算法
JNU freshman4 小时前
图论 之 迪斯科特拉算法求解最短路径
算法·图论
青松@FasterAI5 小时前
【NLP算法面经】本科双非,头条+腾讯 NLP 详细面经(★附面题整理★)
人工智能·算法·自然语言处理
旅僧5 小时前
代码随想录-- 第一天图论 --- 岛屿的数量
算法·深度优先·图论
Emplace5 小时前
ABC381E题解
c++·算法
多敲代码防脱发5 小时前
Spring框架基本使用(Maven详解)
java·网络·后端·spring·maven
若兰幽竹5 小时前
【机器学习】衡量线性回归算法最好的指标:R Squared
算法·机器学习·线性回归