OpenMP 应用程序的实际执行模型在 OpenMP 规范中进行了描述(例如,请参见《OpenMP Application Program Interface, Version 3.0》中的 1.3 节)。但是,该规范未描述对用户可能很重要的一些实现详细信息,Oracle 的实际实现是这样的:通过直接记录的分析信息,用户并不能轻松了解线程是如何交互的。
在任何单线程程序运行时,其调用栈会显示其当前位置,以及如何到达那里的跟踪,跟踪从名为 _start 的例程(该例程调用 main,后者又继续调用程序内的各种子例程)中的起始指令开始。如果子例程包含循环,则程序会重复执行循环内的代码,直至达到循环退出条件。然后继续执行下一个代码序列,依此类推。
通过 OpenMP(或通过自动并行化)并行化程序时,行为是不同的。该并行化程序的直观模型具有像单线程程序那样执行的主线程。当它到达并行循环或并行区域时,将出现其他从属线程(每个都是主线程的克隆),它们都并行执行循环或并行区域的内容,每个从属线程执行不同的工作块。在完成所有工作块时,所有线程都是同步的,从属线程将消失,而主线程继续运行。
并行化程序的实际行为并非如此简单。在 Oracle 实现中,当编译器为并行区域或循环(或任何其他 OpenMP 构造)生成代码时,将提取其内部的代码,并使之成为一个名为 mfunction 的独立函数。(也可以将它称为外联函数或循环体函数。)mfunction 的名称是对 OpenMP 构造类型、从中提取它的函数的名称以及该构造所在源代码行的行号进行编码的结果。这些函数的名称在分析器的专家模式和计算机模式下按以下形式显示,其中方括号中的名称是函数的实际符号表名称:
bardo_ -- OMP parallel region from line 9 [_$p1C9.bardo_] atomsum_ -- MP doall from line 7 [_$d1A7.atomsum_]
还有其他形式的此类函数,它们是从其他源代码构造派生的,名称中的 OMP parallel region 会被替换为 MP construct、MP doall 或 OMP sections。在下面的讨论中,所有这些都统称为并行区域。
执行并行循环内代码的每个线程都可以多次调用其 mfunction,每次调用执行循环内的一个工作块。在所有工作块完成时,每个线程调用库中的同步或归约例程;然后主线程继续,而从属线程变为空闲状态,等待主线程进入下一个并行区域。所有调度和同步都是通过对 OpenMP 运行时的调用处理的。
在其执行过程中,并行区域内的代码可能执行一个工作块,或者它可能与其他线程同步,或者选择要执行的其他工作块。它还可能调用其他函数,而这些函数又可能会再调用其他函数。在并行区域内执行的从属线程(或主线程)可能本身(或者通过它调用的函数)充当主线程,并进入它自己的并行区域,从而导致嵌套并行操作。
分析器基于调用堆栈的统计抽样收集数据,跨所有线程聚集其数据,并针对函数、调用方和被调用方、源代码行和指令,基于所收集数据的类型显示性能度量。分析器以以下三种模式之一提供有关 OpenMP 程序性能的信息:用户模式、专家模式和计算机模式。
有关 OpenMP 程序的数据收集的更多详细信息,请参见 OpenMP 用户社区 Web 站点上的《An OpenMP Runtime API for Profiling》。
分析数据的用户模式显示尝试提供信息,好像程序按照OpenMP 软件执行概述中所述的直观模型实际执行一样。在计算机模式下显示的实际数据捕获运行时库 libmtsk.so(它不对应于模型)的实现详细信息。专家模式显示为匹配模型而改变的混合数据和实际数据。
在用户模式下,更改了分析数据的显示以便更好地匹配模型,在以下三个方面不同于记录的数据和机器模式显示:
从 OpenMP 运行时库的角度来看,构造的人工函数表示每个线程的状态。
处理调用栈以报告对应于代码运行方式模型的数据,如上所述。
为基于时钟的分析实验构造另外两个性能度量,它们分别对应于执行有用工作所用的时间和在 OpenMP 运行时中等待所用的时间。度量为“OpenMP 工作”和“OpenMP 等待”。
对于 OpenMP 3.0 程序,又构造了一个度量“OpenMP 开销”。
构造人工函数,并将其放置在用户模式和专家模式调用堆栈上,以反映线程在 OpenMP 运行时库中处于某个状态的事件。
定义了以下人工函数:
<OMP-overhead> |
在 OpenMP 库中执行 |
<OMP-idle> |
从属线程,等待工作 |
<OMP-reduction> |
执行归约操作的线程 |
<OMP-implicit_barrier> |
在隐式屏障处等待的线程 |
<OMP-explicit_barrier> |
在显式屏障处等待的线程 |
<OMP-lock_wait> |
等待锁定的线程 |
<OMP-critical_section_wait> |
等待进入临界段的线程 |
<OMP-ordered_section_wait> |
等待轮流进入排序段的线程 |
<OMP-atomic_section_wait> |
等待 OpenMP 原子构造的线程。 |
当线程处于对应于其中一个人工函数的 OpenMP 运行时状态时,会将该人工函数作为堆栈上的叶函数添加。当线程的实际叶函数处于 OpenMP 运行时中的任意位置时,<OMP-overhead> 将作为叶函数替换它。否则,从用户模式堆栈中忽略 OpenMP 运行时中的所有 PC。
对于 OpenMP 3.0 程序,不使用 <OMP-overhead> 人工函数。由“OpenMP 开销”度量替换人工函数。
对于 OpenMP 实验,用户模式显示重构的调用堆栈,这些重构的调用堆栈类似于在不使用 OpenMP 的情况下编译程序时获取的调用堆栈。目的在于以与程序的直观了解相匹配的方式提供分析数据,而不是显示实际处理的所有详细信息。当 OpenMP 运行时库正在执行某些特定操作时,协调主线程和从属线程的调用堆栈,并将人工 <OMP-*> 函数添加到调用堆栈。
处理 OpenMP 程序的时钟分析事件时,将显示两个度量(它们分别对应于 OpenMP 系统中的两种状态所用的时间):“OpenMP 工作”和“OpenMP 等待”。
只要从用户代码执行线程(不管串行执行还是并行执行),就会在“OpenMP 工作”中累计时间。只要线程正在等待某项,之后才能继续,就会在“OpenMP 等待”中累计时间,而不管等待是忙等待(自旋等待)还是休眠。这两个度量的总和与时钟分析中的“总 LWP 时间”度量相匹配。
在用户模式、专家模式和计算机模式下显示“OpenMP 等待”和“OpenMP 工作”度量。
查看专家视图模式下的 OpenMP 实验时,如果 OpenMP 运行时正在执行某些特定操作,您将看到格式为 <OMP-*> 的人工函数,这一点类似于用户视图模式。但是,专家视图模式单独显示表示并行化循环、任务等的编译器生成的 mfunction。用户模式会将这些编译器生成的 mfunction 与用户函数聚集在一起。
机器模式显示所有线程和编译器生成的外联函数的本机调用栈。
在执行的各个阶段中,程序的实际调用栈与上面在直观模型中描述的有很大差异。计算机模式将调用堆栈显示为已度量,没有进行转换,且没有构造人工函数。但是,仍显示时钟分析度量。
在下面的每个调用栈中,libmtsk 表示 OpenMP 运行时库内调用栈中的一个或多个帧。就像屏障代码的内部实现或执行归约一样,出现哪些函数以及出现顺序的详细信息随 OpenMP 的发行版的不同而不同。
在第一个并行区域之前
在进入第一个并行区域之前,只有一个线程,即主线程。调用堆栈与用户模式和专家模式下的完全相同。
主线程 |
---|
foo |
main |
_start |
在并行区域中执行时
主线程 |
从属线程 1 |
从属线程 2 |
从属线程 3 |
---|---|---|---|
foo-OMP... | |||
libmtsk | |||
foo |
foo-OMP... |
foo-OMP... |
foo-OMP... |
main |
libmtsk |
libmtsk |
libmtsk |
_start |
_lwp_start |
_lwp_start |
_lwp_start |
在机器模式下,从属线程显示为在 _lwp_start 中启动,而不是在 _start(主线程在其中启动)中启动。(在线程库的某些版本中,该函数可能显示为 _thread_start。对 foo-OMP... 的调用表示为并行化区域生成的 mfunction。
所有线程都在屏障处时
主线程 |
从属线程 1 |
从属线程 2 |
从属线程 3 |
---|---|---|---|
libmtsk | |||
foo-OMP... | |||
foo |
libmtsk |
libmtsk |
libmtsk |
main |
foo-OMP... |
foo-OMP... |
foo-OMP... |
_start |
_lwp_start |
_lwp_start |
_lwp_start |
与线程在并行区域中执行时不同,当线程在屏障处等待时,在 foo 和并行区域代码 foo-OMP... 之间没有来自 OpenMP 运行时的帧。原因是实际执行中不包括 OMP 并行区域函数,但 OpenMP 运行时处理寄存器,以便堆栈展开显示从最后执行的并行区域函数到运行时屏障代码的调用。如果没有它,在机器模式下将无法确定哪个并行区域与屏障调用相关。
离开并行区域之后
主线程 |
从属线程 1 |
从属线程 2 |
从属线程 3 |
---|---|---|---|
foo | |||
main |
libmtsk |
libmtsk |
libmtsk |
_start |
_lwp_start |
_lwp_start |
_lwp_start |
在从属线程中,没有用户帧位于调用栈上。
在嵌套并行区域中时
主线程 |
从属线程 1 |
从属线程 2 |
从属线程 3 |
从属线程 4 |
---|---|---|---|---|
bar-OMP... | ||||
foo-OMP... |
libmtsk | |||
libmtsk |
bar | |||
foo |
foo-OMP... |
foo-OMP... |
foo-OMP... |
bar-OMP... |
main |
libmtsk |
libmtsk |
libmtsk |
libmtsk |
_start |
_lwp_start |
_lwp_start |
_lwp_start |
_lwp_start |