Iri*_*lho 46 c++ multithreading openmp
我开始使用C++使用OpenMP.
我有两个问题:
#pragma omp for schedule?dynamic和之间有什么区别static?请用例子说明.
Hri*_*iev 104
其他人已回答了大部分问题,但我想指出一些特定调度类型比其他调度类型更适合的特定情况.Schedule控制如何在线程之间划分循环迭代.选择正确的时间表会对应用程序的速度产生很大影响.
staticschedule意味着迭代块以循环方式静态映射到执行线程.静态调度的好处是OpenMP运行时保证如果你有两个具有相同迭代次数的独立循环并使用相同数量的线程使用静态调度执行它们,那么每个线程将接收完全相同的迭代范围( s)在两个平行区域.这在NUMA系统上非常重要:如果你在第一个循环中触摸一些内存,它将驻留在执行线程所在的NUMA节点上.然后在第二个循环中,相同的线程可以更快地访问相同的内存位置,因为它将驻留在同一个NUMA节点上.
想象一下,有两个NUMA节点:节点0和节点1,例如两个插座中都带有4核CPU的双插槽Intel Nehalem板.然后线程0,1,2和3将驻留在节点0上,线程4,5,6和7将驻留在节点1上:
| | core 0 | thread 0 |
| socket 0 | core 1 | thread 1 |
| NUMA node 0 | core 2 | thread 2 |
| | core 3 | thread 3 |
| | core 4 | thread 4 |
| socket 1 | core 5 | thread 5 |
| NUMA node 1 | core 6 | thread 6 |
| | core 7 | thread 7 |
Run Code Online (Sandbox Code Playgroud)
每个核心都可以从每个NUMA节点访问内存,但远程访问速度比本地节点访问速度慢(英特尔速度低1.5倍-1.9倍).你运行这样的东西:
char *a = (char *)malloc(8*4096);
#pragma omp parallel for schedule(static,1) num_threads(8)
for (int i = 0; i < 8; i++)
memset(&a[i*4096], 0, 4096);
Run Code Online (Sandbox Code Playgroud)
在这种情况下,4096字节是x86上Linux上的一个内存页面的标准大小,如果不使用大页面的话.此代码将整个32 KiB阵列归零a.该malloc()调用仅保留虚拟地址空间,但实际上并未"触摸"物理内存(这是默认行为,除非malloc使用其他版本的内容,例如将内存归零的内容calloc()).现在这个数组是连续的,但只在虚拟内存中.在物理内存中,它的一半位于连接到套接字0的内存中,一半位于连接到套接字1的内存中.这是因为不同的部分被不同的线程归零,并且这些线程驻留在不同的内核上,并且存在称为第一触摸的内容 NUMA策略意味着在NUMA节点上分配存储器页面,在该NUMA节点上首先"触摸"存储器页面的线程驻留在NUMA节点上.
| | core 0 | thread 0 | a[0] ... a[4095]
| socket 0 | core 1 | thread 1 | a[4096] ... a[8191]
| NUMA node 0 | core 2 | thread 2 | a[8192] ... a[12287]
| | core 3 | thread 3 | a[12288] ... a[16383]
| | core 4 | thread 4 | a[16384] ... a[20479]
| socket 1 | core 5 | thread 5 | a[20480] ... a[24575]
| NUMA node 1 | core 6 | thread 6 | a[24576] ... a[28671]
| | core 7 | thread 7 | a[28672] ... a[32768]
Run Code Online (Sandbox Code Playgroud)
现在让我们运行另一个这样的循环:
#pragma omp parallel for schedule(static,1) num_threads(8)
for (i = 0; i < 8; i++)
memset(&a[i*4096], 1, 4096);
Run Code Online (Sandbox Code Playgroud)
每个线程将访问已经映射的物理内存,并且它将具有与第一个循环期间的线程到内存区域相同的映射.这意味着线程将只访问位于其本地内存块中的内存,这将很快.
现在假设另一个调度方案用于第二个循环:schedule(static,2).这将把迭代空间"切"成两次迭代的块,总共将有4个这样的块.会发生什么是我们将有以下线程到内存位置映射(通过迭代号):
| | core 0 | thread 0 | a[0] ... a[8191] <- OK, same memory node
| socket 0 | core 1 | thread 1 | a[8192] ... a[16383] <- OK, same memory node
| NUMA node 0 | core 2 | thread 2 | a[16384] ... a[24575] <- Not OK, remote memory
| | core 3 | thread 3 | a[24576] ... a[32768] <- Not OK, remote memory
| | core 4 | thread 4 | <idle>
| socket 1 | core 5 | thread 5 | <idle>
| NUMA node 1 | core 6 | thread 6 | <idle>
| | core 7 | thread 7 | <idle>
Run Code Online (Sandbox Code Playgroud)
这里发生了两件坏事:
因此,使用静态调度的一个优点是它可以改善内存访问的局部性.缺点是调度参数的错误选择会破坏性能.
dynamic调度工作以"先到先得"为基础.具有相同数量线程的两次运行可能(并且很可能会)产生完全不同的"迭代空间" - >"线程"映射,因为可以轻松验证:
$ cat dyn.c
#include <stdio.h>
#include <omp.h>
int main (void)
{
int i;
#pragma omp parallel num_threads(8)
{
#pragma omp for schedule(dynamic,1)
for (i = 0; i < 8; i++)
printf("[1] iter %0d, tid %0d\n", i, omp_get_thread_num());
#pragma omp for schedule(dynamic,1)
for (i = 0; i < 8; i++)
printf("[2] iter %0d, tid %0d\n", i, omp_get_thread_num());
}
return 0;
}
$ icc -openmp -o dyn.x dyn.c
$ OMP_NUM_THREADS=8 ./dyn.x | sort
[1] iter 0, tid 2
[1] iter 1, tid 0
[1] iter 2, tid 7
[1] iter 3, tid 3
[1] iter 4, tid 4
[1] iter 5, tid 1
[1] iter 6, tid 6
[1] iter 7, tid 5
[2] iter 0, tid 0
[2] iter 1, tid 2
[2] iter 2, tid 7
[2] iter 3, tid 3
[2] iter 4, tid 6
[2] iter 5, tid 1
[2] iter 6, tid 5
[2] iter 7, tid 4
Run Code Online (Sandbox Code Playgroud)
(当gcc使用时会观察到相同的行为)
如果来自该static部分的示例代码是使用dynamic调度运行的,那么原始位置将被保留的概率仅为1/70(1.4%),并且将发生远程访问的机会为69/70(98.6%).这个事实经常被忽视,因此实现了次优的性能.
选择static和dynamic调度之间还有另一个原因- 工作负载平衡.如果每次迭代与要完成的平均时间有很大不同,那么在静态情况下可能会出现高工作不平衡.以完成迭代的时间随迭代次数线性增长的情况为例.如果迭代空间在两个线程之间静态划分,则第二个线程将比第一个线程具有多三倍的工作量,因此第一个线程将空闲的计算时间的2/3.动态计划引入了一些额外的开销,但在特定情况下将导致更好的工作负载分配.一种特殊的dynamic调度是guided随着工作的进展,为每个任务提供越来越小的迭代块.
由于预编译的代码可以在各种平台上运行,因此最终用户可以控制调度会很好.这就是OpenMP提供特殊schedule(runtime)条款的原因.通过runtime调度,类型取自环境变量的内容OMP_SCHEDULE.这允许在不重新编译应用程序的情况下测试不同的调度类型,并允许最终用户对他或她的平台进行微调.
Eug*_*ica 23
我认为误解来自于你错过了关于OpenMP的观点这一事实.在一个句子中,OpenMP允许您通过启用并行性来更快地执行程序.在程序中,可以通过多种方式启用并行性,其中一种方法是使用线程.假设你有和数组:
[1,2,3,4,5,6,7,8,9,10]
Run Code Online (Sandbox Code Playgroud)
并且您希望在此数组中将所有元素增加1.
如果你要使用
#pragma omp for schedule(static, 5)
Run Code Online (Sandbox Code Playgroud)
这意味着每个线程将被分配5个连续的迭代.在这种情况下,第一个线程将采用5个数字.第二个将采用另外5个,依此类推,直到没有更多数据要处理或达到最大线程数(通常等于核心数).在编译期间共享工作负载.
的情况下
#pragma omp for schedule(dynamic, 5)
Run Code Online (Sandbox Code Playgroud)
工作将在线程之间共享,但此过程将在运行时进行.因此涉及更多的开销.第二个参数指定数据块的大小.
对OpenMP不是很熟悉我冒险假设当编译代码要在具有与编译代码的配置不同的配置的系统上运行时,动态类型更合适.
我会推荐下面的页面,其中讨论了用于并行化代码,前提条件和限制的技术
https://computing.llnl.gov/tutorials/parallel_comp/
其他链接:
http://en.wikipedia.org/wiki/OpenMP
C中openMP中静态和动态计划的区别
http://openmp.blogspot.se/
循环分区方案是不同的.静态调度器将N个元素上的循环划分为M个子集,然后每个子集将严格包含N/M个元素.
动态方法可以动态计算子集的大小,如果子集的计算时间不同,这可能很有用.
如果计算时间变化不大,则应使用静态方法.
| 归档时间: |
|
| 查看次数: |
69298 次 |
| 最近记录: |