OpenMP中“静态”和“动态”计划之间的区别是什么?

时间:2012-06-01 12:21:07

标签: c++ multithreading openmp

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

我有两个问题:

  1. 什么是#pragma omp for schedule
  2. dynamicstatic之间的区别是什么?
  3. 请用例子说明。

3 个答案:

答案 0 :(得分:100)

其他人已经回答了大部分问题,但我想指出一些特定调度类型比其他调度类型更适合的特定情况。 Schedule控制如何在线程之间划分循环迭代。选择正确的时间表会对应用程序的速度产生很大影响。

static schedule表示迭代块以循环方式静态映射到执行线程。静态调度的好处是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()电话只保留虚拟地址空间,但实际上并没有触摸&#34;物理内存(除非使用其他版本的malloc,否则这是默认行为,例如,像calloc()那样将内存归零的内容。现在这个数组是连续的,但只在虚拟内存中。在物理内存中,它的一半位于连接到套接字0的内存中,一半位于连接到套接字1的内存中。这是因为不同的部分被不同的线程归零,并且这些线程驻留在不同的内核上,并且有一些名为首先触摸 NUMA策略,这意味着在NUMA节点上分配内存页面,在该NUMA节点上首先触及的线程&#34;内存页面驻留。

|             | 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);

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

现在假设另一个调度方案用于第二个循环:schedule(static,2)。这将&#34;砍&#34;将迭代空间迭代到两次迭代的块中,总共将有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将保持空闲。

因此,使用静态调度的一个优点是它可以改善内存访问的局部性。缺点是调度参数选择不当会破坏性能。

dynamic日程安排适用于&#34;先来先到先得&#34;基础。具有相同数量线程的两次运行可能(并且很可能会)产生完全不同的“迭代空间”#34; - &GT; &#34;螺纹&#34;可以轻松验证映射:

$ 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调度之间进行选择还有另一个原因 - 工作负载平衡。如果每次迭代与要完成的平均时间有很大不同,那么在静态情况下可能会出现高工作不平衡。以完成迭代的时间随迭代次数线性增长的情况为例。如果迭代空间在两个线程之间静态划分,则第二个线程将比第一个线程具有多三倍的工作量,因此第一个线程将空闲的计算时间的2/3。动态计划引入了一些额外的开销,但在特定情况下将导致更好的工作负载分配。一种特殊的dynamic调度是guided,随着工作的进展,每个任务都会有越来越小的迭代块。

由于预编译代码可以在各种平台上运行,因此最终用户可以控制调度会很好。这就是OpenMP提供特殊schedule(runtime)子句的原因。使用runtime计划,类型取自环境变量OMP_SCHEDULE的内容。这允许在不重新编译应用程序的情况下测试不同的调度类型,并允许最终用户对他或她的平台进行微调。

答案 1 :(得分:22)

我认为误解来自于你错过了关于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不是很熟悉当编译代码要在与编译代码的配置不同的系统上运行时,我冒险假设动态类型更合适。

我会推荐下面的页面,其中讨论了用于并行化代码,前提条件和限制的技术

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

其他链接
http://en.wikipedia.org/wiki/OpenMP
Difference between static and dynamic schedule in openMP in C
http://openmp.blogspot.se/

答案 2 :(得分:6)

循环分区方案不同。静态调度程序将N个元素上的循环划分为M个子集,然后每个子集将严格包含N / M个元素。

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

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