最近有在做一些跟多线程相关的事情,不可避免的就会接触到 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 方式的选择(理论上)
-
将任务并行化需要满足三条原则
- 分解代价低:也就是获取不同的 chunk 的时候的代价,这个代价主要来自于两部分
- 创建线程
- schedule 方式引入的 overhead
- 在不同的线程中计算量要均衡,否则会因为木桶效应导致执行最长的线程影响整个任务的执行时间,影响这个指标的原因有:
- 每个线程获得的 chunk 数量是否一致(是否能刚好均分)
- loop body 中的计算量在每个循环中是否一致,比如第一个循环执行一个加法,第100个循环执行 100 个加法
- 计算能力的差异,比如 CPU P core 和 E core,以及内存的 NUMA节点等
- 避免 cache 冲突:(具体可以看多线程的 false sharing 问题)这个跟 scheduler 关系不大,主要看 chunksize 的选择
- 分解代价低:也就是获取不同的 chunk 的时候的代价,这个代价主要来自于两部分
-
而对于两种 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 当然没有意义了,这种情况就要提示你应该改改你的代码了
一些学习资料:
- OpenMP 视频教程:www.youtube.com/watch?v=nE-...
- 《OpenMP编译原理及实现技术》:本文代码和图片取自该书