每个多任务操作系统都有一个叫做进程调度器的东西。这是一个操作系统组件,它决定在何处以及何时运行每个进程。调度程序通常在他们做出的决定中非常固执,但这些决定通常会受到用户提供的各种策略和提示的影响。几乎所有调度程序的默认配置都是尝试将负载分散到所有可用的 CPU 上,这通常会导致进程从一个 CPU 迁移到另一个 CPU。幸运的是,除了“最先进的桌面操作系统”(又名 OS X)之外的任何现代操作系统都支持所谓的处理器亲和性。每个进程都有一组允许在其上执行的处理器——即该进程的所谓 CPU 亲和性集。通过为各种进程配置不相交的关联集,可以使这些进程同时执行,而不会相互占用 CPU 时间。Linux、FreeBSD(使用 ULE 调度程序)、Windows NT(这也包括自 Windows XP 以来的所有桌面版本)以及可能的其他操作系统(但不包括 OS X)支持显式 CPU 关联。然后,每个操作系统都提供一组内核调用来操纵亲缘关系,并且还提供了一种无需编写特殊程序即可执行此操作的工具。在 Linux 上,这是使用sched_setaffinity(2)
系统调用和taskset
命令行工具。也可以通过创建cpuset
实例来控制亲和力。在 Windows 上,可以在任务管理器中从给定进程的上下文菜单中设置SetProcessAffinityMask()
和/或和关联。也可以在启动新进程时SetThreadAffinityMask()
将所需的关联掩码指定为 shell 命令的参数。START
这一切都与 OpenMP 相关的是,所列操作系统的大多数 OpenMP 运行时都支持以一种形式或另一种方式为每个 OpenMP 线程指定所需的 CPU 亲和性。最简单的控制是OMP_PROC_BIND
环境变量。这是一个简单的开关 - 当设置为 时TRUE
,它指示 OpenMP 运行时“绑定”每个线程,即给它一个只包含单个 CPU 的关联集。线程到 CPU 的实际位置取决于实现,并且每个实现都提供了自己的控制方式。例如,GNU OpenMP 运行时 ( libgomp
) 读取GOMP_CPU_AFFINITY
环境变量,而 Intel OpenMP 运行时(不久前开源)读取KMP_AFFINITY
环境变量。
这里的基本原理是,您可以限制程序的亲和性,以便仅使用所有可用 CPU 的子集。剩下的进程将主要被安排到其余的 CPU,尽管只有手动设置它们的关联性才能保证这一点(这只有在您具有 root/管理员访问权限时才可行,否则您只能修改进程的关联性你拥有)。
值得一提的是,通常(但并非总是)使用比关联集中的 CPU 数量更多的线程运行是没有意义的。例如,如果您将程序限制为在 60 个 CPU 上运行,那么使用 64 个线程将导致某些 CPU 被超额订阅并导致线程之间的时间共享。这将使某些线程比其他线程运行得慢。大多数 OpenMP 运行时的默认调度是schedule(static)
因此并行区域的总执行时间由最慢线程的执行时间决定。如果一个线程与另一个线程分时共享,那么两个线程的执行速度都会比不分时的线程慢,并且整个并行区域都会延迟。这不仅降低了并行性能,而且还导致浪费周期,因为更快的线程只会等待什么都不做(可能忙于在并行区域末尾的隐式屏障处循环)。解决方案是使用动态调度,即:
#pragma omp parallel for schedule(dynamic,chunk_size)
for (int out = 1; out <= matrix.rows; out++)
{
...
}
其中chunk_size
是每个线程获得的迭代块的大小。整个迭代空间被划分为chunk_size
迭代块,并按照先到先得的原则分配给工作线程。块大小是一个重要参数。如果它太低(默认值为 1),则 OpenMP 运行时管理动态调度可能会产生巨大的开销。如果它太高,那么每个线程可能没有足够的工作可用。块大小大于maxtrix.rows / #threads
.
动态调度允许您的程序适应可用的 CPU 资源,即使它们不是统一的,例如,如果有其他进程正在运行并且与当前进程共享时间。但它有一个问题:像你的 64 核这样的大系统通常是 ccNUMA(缓存一致的非统一内存访问)系统,这意味着每个 CPU 都有自己的内存块并可以访问其他 CPU 成本高(例如需要更多时间和/或提供更少带宽)。动态调度往往会破坏数据的局部性,因为无法确定驻留在一个 NUMA 上的内存块不会被另一个 NUMA 节点上运行的线程使用。当数据集很大并且不适合 CPU 缓存时,这一点尤其重要。因此 YMMV。