D. schedule 子句
并行区域至少在其末尾处有一个屏障,并且区域中可能有其他障碍。 在每个屏障处,团队的其他成员必须等待最后一个线程到达。 为了最大程度地减少此等待时间,应分布共享工作,以便所有线程大约同时到达屏障。 如果该共享工作的某个部分包含在 for
构造中,则 schedule
子句可用于此用途。
当重复引用相同对象时,for
构造的计划选择可能主要取决于内存系统的特征,例如缓存是否存在和大小以及内存访问时间是一致还是非一致的。 这类考虑可能会使每个线程在一系列循环中一致地引用数组的同一组元素更为可取(即使某些线程在某些循环中分配的工作量相对较少)。 可以使用对所有循环具有相同边界的 static
计划来完成此设置。 在下面的示例中,第二个循环中使用零作为下限,即使 k
在计划不重要时会更自然。
#pragma omp parallel
{
#pragma omp for schedule(static)
for(i=0; i<n; i++)
a[i] = work1(i);
#pragma omp for schedule(static)
for(i=0; i<n; i++)
if(i>=k) a[i] += work2(i);
}
在其余示例中,假设内存访问不是主要考虑因素。 除非另有说明,否则假设所有线程都会收到类似的计算资源。 在这些情况下,for
构造的计划选择取决于要在最近的前一个屏障与隐式关闭屏障或最近的即将到来屏障(如果有 nowait
子句)之间执行的所有共享工作。 对于每种类型的计划,一个简短示例演示了该计划类型如何可能是最佳选择。 每个示例后都进行了简要讨论。
static
计划也适用于最简单的情况,即包含单个 for
构造的并行区域,每个迭代都需要相同的工作量。
#pragma omp parallel for schedule(static)
for(i=0; i<n; i++) {
invariant_amount_of_work(i);
}
static
计划的特征是每个线程获取的迭代数与任何其他线程大致相同,并且每个线程可以独立确定分配给它的迭代。 因此,不需要同步来分布工作,并且在每个迭代需要相同工作量的假设下,所有线程应在大约相同的时间完成。
对于 p 线程团队,让 ceiling(n/p) 为整数 q,这满足 n = p*q - r 与 0 <= r < p。 此示例的 static
计划的一个实现会将 q 个迭代分配给前 p-1 个线程,将 q-r 个迭代分配给最后一个线程。 另一个可接受的实现会将 q 个迭代分配给前 p-r 个线程,将 q-1 个迭代分配给其余 r 个线程。 此示例说明为什么程序不应依赖于特定实现的细节。
dynamic
计划适用于其迭代需要不同甚至不可预知的工作量的 for
构造的情况。
#pragma omp parallel for schedule(dynamic)
for(i=0; i<n; i++) {
unpredictable_amount_of_work(i);
}
dynamic
计划的特征是没有线程在屏障上等待的时间超过其他线程执行其最终迭代的时间。 此要求意味着必须随着线程可用而将迭代逐一分配给线程,并且对每次分配进行同步。 可以通过指定大于 1 的最小区块大小 k 来减少同步开销,以便一次为线程分配 k,直到剩余块大小少于 k。 这可以保证没有线程在屏障上等待的时间超过其他线程执行其(至多)k 个迭代的最终区块的时间。
如果线程接收不同的计算资源(这与每个迭代具有不同工作量的效果大致相同),则 dynamic
计划可能非常有用。 同样,如果线程以不同时间到达 for
构造,则动态计划也可能非常有用,但在其中某些情况下,guided
计划可能更好。
guided
计划适用于线程以不同时间到达 for
构造,每个迭代需要大约相同的工作量的情况。 例如,如果 for
构造前面有一个或多个节或是带有 nowait
子句的 for
构造,则可能会出现这种情况。
#pragma omp parallel
{
#pragma omp sections nowait
{
// ...
}
#pragma omp for schedule(guided)
for(i=0; i<n; i++) {
invariant_amount_of_work(i);
}
}
与 dynamic
一样,guided
计划可保证没有线程在屏障上等待的时间超过其他线程执行其最终迭代或最终 k 个迭代(如果指定了区块大小 k)的时间。 在这些计划中,guided
计划的特征是它需要的同步最少。 对于区块大小 k,典型实现会将 q = ceiling(n/p) 个迭代分配给第一个可用线程,将 n 设置为 n-q 和 p*k 中的较大值,并在分配了所有迭代之前重复。
当最佳计划的选择不像这些示例那样清晰时,runtime
计划便于试验不同的计划和区块大小,而无需修改和重新编译程序。 当最佳计划取决于(以某种可预测的方式)应用了程序的输入数据时,它也非常有用。
若要查看不同计划之间的权衡示例,请考虑在 8 个线程之间共享 1000 个迭代。 假设每个迭代中的工作量不变,并将其用作时间单位。
如果所有线程同时启动,则 static
计划会使构造执行 125 个单位,不进行同步。 但假设一个线程在 100 个单位后到达。 其余七个线程因而在屏障处等待 100 个单位,整个构造的执行时间增加到 225。
由于 dynamic
和 guided
计划都可确保没有线程在屏障上等待多个单位,因此延迟线程会导致构造的执行时间仅增加到 138 个单位(可能是由于同步延迟而增加)。 如果此类延迟不能忽略不计,则在假设默认区块大小为 1 的情况下,同步数对于 dynamic
为 1000,但对于 guided
只有 41,这一点十分重要。 在区块大小为 25 的情况下,dynamic
和 guided
都以 150 个单位完成,加上所需同步的任何延迟(现在数字分别仅为 40 和 20)。