首页 > 解决方案 > OpenMp:如何确保每个线程在动态调度中至少工作 1 次迭代

问题描述

我正在为循环迭代使用动态调度。但是当每次迭代的工作量太小时,一些线程不工作或线程数量巨大时。例如。有 100 次迭代,有 90 个线程,我希望每个线程至少进行一次迭代,其余 10 次迭代可以分配给完成工作的线程。我怎样才能做到这一点?

标签: dynamicopenmp

解决方案


您不能强制 OpenMP 运行时执行此操作。但是,您可以向 OpenMP 运行时提供提示,以便它可能会在(它决定)可能以更高的开销为代价时这样做。方法是指定动态调度循环的粒度。这是一个例子:

#pragma omp parallel for schedule(dynamic,1)
for(int i=0 ; i<100 ; ++i)
    compute(i);

使用这样的代码,运行时可以自由地在线程之间均匀地共享工作(使用工作共享调度程序)或让线程窃取驱动并行计算的主线程的工作(使用工作窃取调度程序)。在第二种方法中,虽然粒度是 1 次循环迭代,但一些线程可能会窃取比它们实际需要的更多的工作(例如,为了提高性能)。如果循环迭代足够快,则线程之间的工作可能不会平衡。

创建 90 个线程的成本很高,向 90 个线程发送工作也远非免费,因为它主要受原子操作相对较高的延迟、它们的可销售性以及唤醒线程的延迟的限制。此外,虽然从用户的角度来看,这样的操作似乎是同步的,但实际上并非如此(尤其是在 90 个线程和基于多套接字 NUMA 的架构上)。结果,一些线程可能完成计算循环的一次迭代,而其他线程可能不知道并行计算甚至尚未创建。使线程知道要完成的计算的开销通常随着使用的线程数量的增加而增加。在某些情况下,这种开销可能高于实际计算,使用更少的线程会更有效。

OpenMP 运行时开发人员有时应该通过较小的通信开销来平衡工作。因此,这些决定在您的情况下可能表现不佳,但可以提高其他类型应用程序的可销售性。在工作窃取调度程序(例如 Clang/ICC OpenMP 运行时)上尤其如此。请注意,提高 OpenMP 运行时的可扩展性是一个正在进行的研究领域。

我建议您尝试多个 OpenMP 运行时(包括可能适合或不适合在生产代码中使用的研究运行时)。您还可以使用OMP_WAIT_POLICY变量来减少唤醒线程的开销。您还可以尝试使用 OpenMP 任务来强制运行时不合并迭代。我还建议您分析您的代码以查看发生了什么并找到潜在的软件/硬件瓶颈。

更新

如果您使用的 OpenMP 线程多于机器上的硬件线程,则处理器无法同时执行它们(它只能在每个硬件线程上执行一个 OpenMP 线程)。因此,您计算机上的操作系统会在硬件线程上调度 OpenMP 线程,以便从用户的角度来看它们似乎是同时执行的。但是,它们不会同时运行,而是在非常短的时间段(例如 100 毫秒)内以交错的方式执行。

例如,如果您有一个具有 8 个硬件线程的处理器并且您使用 8 个 OpenMP 线程,您可以粗略地假设它们将同时运行。但是如果您使用 16 个 OpenMP 线程,您的操作系统可以选择使用以下方式来调度它们:

  • 前 8 个线程执行 100 毫秒;
  • 最后 8 个线程执行 100 毫秒;
  • 前 8 个线程再次执行 100 毫秒;
  • 最后 8 个线程再次执行 100 毫秒;
  • 等等

如果您的计算持续时间少于 100 毫秒,则 OpenMP 动态/引导式调度程序会将最后 8 个线程的工作转移到前 8 个线程,以便整体执行时间更快。因此,前 8 个线程可以执行所有工作,而后 8 个线程一旦执行就没有任何东西可以执行。这就是线程间工作不平衡的原因。

因此,如果您想衡量 OpenMP 程序的性能,您不应使用比硬件线程更多的 OpenMP 线程(除非您确切地知道自己在做什么并且完全了解这种影响)。


推荐阅读