OpenMP中的“静态”和“dynamic”日程安排有什么区别?

我开始使用C ++使用OpenMP。

我有两个问题:

  1. 什么是#pragma omp for schedule
  2. dynamicstatic什么区别?

请用例子来解释一下。

其他人已经回答了大部分的问题,但是我想指出一些特定的情况,其中一个特定的调度types比其他调度types更适合。 时间表控制循环迭代在线程间如何分配。 select正确的时间表对应用程序的速度有很大的影响。

static调度表示迭代块以循环方式静态映射到执行线程。 静态调度的好处在于,OpenMP运行时保证,如果您有两个具有相同迭代次数的独立循环,并使用静态调度以相同数量的线程执行它们,则每个线程将接收完全相同的迭代范围( s)在两个平行区域。 这在NUMA系统中非常重要:如果在第一个循环中触及某些内存,它将驻留在执行线程所在的NUMA节点上。 然后在第二个循环中,同一个线程可以更快地访问相同的内存位置,因为它将驻留在同一个NUMA节点上。

假设有两个NUMA节点:节点0和节点1,例如双插槽的Intel Nehalem主板,两个插槽都带有4核CPU。 然后,线程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 | 

每个核心可以从每个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); 

在这种情况下,4096字节是x86上Linux上的一个内存页面的标准大小,如果不使用大页面的话。 这段代码将整个32 KiB数组a归零。 malloc()调用仅保留虚拟地址空间,但实际上并不“触及”物理内存(这是默认行为,除非使用某种其他版本的malloc ,例如像calloc()那样将内存归零)。 现在这个数组是连续的,但只在虚拟内存中。 在物理内存中,一半位于连接到套接字0的内存中,一半位于连接到套接字1的内存中。这是因为不同的部分被不同的线程归零,而这些线程驻留在不同的核心上,并且有一些称为第一次触摸 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] 

现在让我们运行另一个循环:

 #pragma omp parallel for schedule(static,1) num_threads(8) for (i = 0; i < 8; i++) memset(&a[i*4096], 1, 4096); 

每个线程将访问已映射的物理内存,并且它将具有与第一个循环期间相同的线程到内存区域的映射。 这意味着线程将只访问位于本地内存块中的内存,这些内存块将会很快。

现在想象另一个调度scheme用于第二个循环: 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> 

这里发生两件不好的事情:

  • 线程4到7保持空闲,一半的计算能力丢失;
  • 线程2和3访问非本地内存,它将花费大约两倍的时间来完成,在此期间线程0和1将保持空闲状态。

所以使用静态调度的好处之一是提高了内存访问的局部性。 缺点是调度参数的错误select会破坏性能。

dynamic调度以“先到先得”为基础。 使用相同数量的线程的两次运行可能(并且很可能会)产生完全不同的“迭代空间” – >“线程”映射,因为人们可以轻松validation:

 $ 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 

(当使用gcc时,观察到相同的行为)

如果static部分的示例代码是使用dynamic调度运行的,那么保留原始位置的可能性只有1/70(1.4%),远程访问的可能性是69/70(98.6%)。 这个事实经常被忽略,因此达不到最佳性能。

staticdynamic调度之间进行select还有另一个原因 – 工作负载平衡。 如果每次迭代与完成平均时间大不相同,那么在静态情况下可能出现高度的工作不平衡。 以一个迭代次数与迭代次数成线性增长的情况为例。 如果迭代空间在两个线程之间被静态分割,则第二个线程将比第一个线程多三倍,因此在计算时间的2/3时,第一个线程将空闲。 dynamic调度引入了一些额外的开销,但在这种情况下会导致更好的工作量分配。 一种特殊的dynamic调度就是在工作进行时为每个任务提供越来越小的迭代块的guided

由于预编译的代码可以在各种平台上运行,所以如果最终用户能够控制调度,那将是很好的。 这就是OpenMP提供特殊schedule(runtime)子句的原因。 使用runtime调度,types取自环境variablesOMP_SCHEDULE的内容。 这允许在不重新编译应用程序的情况下testing不同的调度types,并允许最终用户为他或她的平台进行微调。

我认为这个误解来自于你错过了关于OpenMP的观点。 OpenMP允许您通过启用并行性来更快地执行您的程序。 在一个程序中,并行可以通过多种方式来启用,其中一个是通过使用线程。 假设你有和数组:

 [1,2,3,4,5,6,7,8,9,10] 

你想在这个数组中增加1的所有元素。

如果你打算使用

 #pragma omp for schedule(static, 5) 

这意味着每个线程将被分配5个连续的迭代。 在这种情况下,第一个线程将需要5个数字。 第二个将需要另外5个,依此类推,直到没有更多的数据要处理或达到线程的最大数量(通常等于核心数量)。 在编译过程中共享工作量。

的情况下

 #pragma omp for schedule(dynamic, 5) 

工作将在线程之间共享,但是这个过程将在运行时发生。 因此涉及更多的开销。 第二个参数指定数据块的大小。

对OpenMP不太熟悉我冒险认为,dynamictypes更适合于编译代码将在具有与编译代码的configuration不同的configuration的系统上运行。

我会build议下面的页面讨论用于并行化代码,前提条件和限制的技术

https://computing.llnl.gov/tutorials/parallel_comp/

其他链接
http://en.wikipedia.org/wiki/OpenMP
C中openMP的静态和dynamic调度的区别
http://openmp.blogspot.se/

循环分区scheme是不同的。 静态调度器将N个元素上的一个循环划分为M个子集,每个子​​集将包含严格的N / M个元素。

dynamic方法计算dynamic子集的大小,如果子集的计算时间不同,这可能很有用。

如果计算时间变化不大,应该使用静态方法。