static调度意味着迭代块以循环方式静态映射到执行线程。静态调度的好处是 OpenMP 运行时保证,如果您有两个具有相同迭代次数的独立循环,并且使用静态调度使用相同数量的线程执行它们,那么每个线程将在两个并行区域接收完全相同的迭代范围。这在 NUMA 系统中非常重要: 如果在第一个循环中触及某些内存,它将驻留在执行线程所在的 NUMA 节点上。然后在第二个循环中,相同的线程可以更快地访问相同的内存位置,因为它将驻留在相同的 NUMA 节点上。
假设有两个 NUMA 节点: 节点0和节点1,例如一个双套接字 Intel Nehalem 板,两个套接字中都有4核 CPU。然后,线程0、1、2和3将驻留在节点0上,而线程4、5、6和7将驻留在节点1上:
每个核心都可以从每个 NUMA 节点访问内存,但远程访问比本地节点访问慢(Intel 上为1.5 x-1.9 x)。如果你这么做:
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 上一个内存页面的标准大小。这段代码将整个32KiB 数组 a归零。malloc()调用只保留了虚拟地址空间,但实际上并没有“触及”物理内存(这是默认行为,除非使用其他版本的 malloc,例如像 calloc()那样对内存进行归零)。现在这个数组是连续的,但只在虚拟内存中。在物理内存中,它的一半位于连接到套接字0的内存中,另一半位于连接到套接字1的内存中。这是因为不同的部分被不同的线程归零,这些线程位于不同的核心上,有一个叫做 第一次接触 NUMA 策略的东西,这意味着内存页分配在 NUMA 节点上,第一个“接触”内存页的线程所在的节点上。
$ 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