OpenMP 应用程序的实际执行模型在 OpenMP 规范中进行了描述(例如,请参见《OpenMP Application Program Interface, Version 2.5》 的 1.3 节。)但是,该规范未描述对用户可能很重要的一些实现详细信息,Sun Microsystems 的实际实现是这样的:通过直接记录的分析信息,用户并不能轻松了解线程是如何交互的。
在任何单线程程序运行时,其调用栈会显示其当前位置,以及如何到达那里的跟踪,跟踪从名为 _start 的例程(该例程调用 main,后者又继续调用程序内的各种子例程)中的起始指令开始。如果子例程包含循环,则程序会重复执行循环内的代码,直至达到循环退出条件。然后继续执行下一个代码序列,依此类推。
通过 OpenMP(或通过自动并行化)并行化程序时,行为是不同的。该行为的直观模型具有像单线程程序那样执行的主线程。当它到达并行循环或并行区域时,将出现其他从属线程(每个都是主线程的克隆),它们都并行执行循环或并行区域的内容,每个从属线程执行不同的工作块。在完成所有工作块时,所有线程都是同步的,从属线程将消失,而主线程继续运行。
当编译器为并行区域或循环(或任何其他 OpenMP 构造)生成代码时,将提取其内部的代码,并使之成为一个名为 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 用户社区 Web 站点上的白皮书《An OpenMP Runtime API for Profiling》。
分析数据的用户模式显示尝试提供信息,好像程序按照OpenMP 软件执行概述中所述的模型实际执行一样。实际的数据捕获运行时库 libmtsk.so(它不对应于模型)的实现详细信息。在用户模式下,更改了分析数据的显示以便更好地匹配模型,在以下三个方面不同于记录的数据和机器模式显示:
从 OpenMP 运行时库的角度来看,构造的人工函数表示每个线程的状态。
处理调用栈以报告对应于代码运行方式模型的数据,如上所述。
为基于时钟的分析实验构造另外两个性能度量,它们分别对应于执行有用工作所用的时间和在 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>-等待轮流进入排序段的线程
当线程处于对应于其中一个函数的 OpenMP 运行时状态时,会将对应函数作为堆栈上的叶函数添加。当线程的叶函数处于 OpenMP 运行时中的任意位置时,<OMP-overhead> 将作为叶函数替换它。否则,从用户模式堆栈中忽略 OpenMP 运行时中的所有 PC。
了解此模型的最简单方法是查看 OpenMP 程序在其执行过程中各个时刻的调用栈。本节介绍一个简单的程序,该程序具有调用一个子例程 foo 的主程序。该子例程具有单个并行循环,线程在其中完成工作、争用、获取和释放锁,以及进入和离开临界段。显示另一组调用栈,反映一个从属线程调用了另一函数 bar(它进入嵌套并行区域)时的状态。
在此显示中,并行区域中所用的所有包含时间都包括在从中提取它的函数的包含时间中,其中包括在 OpenMP 运行时中所用的时间,而且该包含时间一直向上传播到 main 和 _start。
表示此模型中行为的调用栈如后续小节中所示。并行区域函数的实际名称具有以下形式,如上所述:
foo -- OMP parallel region from line 9[ [_$p1C9.foo] bar -- OMP parallel region from line 5[ [_$p1C5.bar]
为了清晰起见,在说明中使用以下简化形式:
foo -- OMP... bar -- OMP...
在说明中,所有线程的调用栈都在执行程序期间的某个时刻显示。每个线程的调用栈均显示为帧堆栈,将在分析器时间线标签中为单个线程选择单独分析事件得到的数据与顶部的叶 PC 匹配。在时间线标签中,每个帧都显示有 PC 偏移(在下面省略了此偏移)。所有线程的堆栈都在水平数组中显示,而在分析器时间线标签中,其他线程的堆栈将出现在垂直堆叠的分析数据栏中。此外,在提供的表示法中,将显示所有线程的堆栈,就像它们是在同一时刻捕获的那样,而在实际实验中,堆栈在每个线程中独立地捕获,并且彼此之间可能存在相对偏离。
所示的调用栈表示的数据如在分析器中或在 er_print 实用程序中通过用户视图模式显示的一样。
在第一个并行区域之前
在进入第一个并行区域之前,只有一个线程,即主线程。
主线程 |
---|
foo |
main |
_start |
进入第一个并行区域时
此时,库已创建从属线程,而且所有线程(主线程和从属线程)即将开始处理其工作块。所有线程都显示为从构造的 OpenMP 指令所在行上的 foo,或者从包含已自动并行化的循环语句的行,调用到并行区域 foo-OMP... 的代码中。每个线程中并行区域的代码从并行区域中的第一个指令调用到 OpenMP 支持库(显示为 <OMP-overhead> 函数)中。
主线程 |
从属线程 1 |
从属线程 2 |
从属线程 3 |
---|---|---|---|
<OMP-overhead> |
<OMP-overhead> |
<OMP-overhead> |
<OMP-overhead> |
foo-OMP... |
foo-OMP... |
foo-OMP... |
foo-OMP... |
foo |
foo |
foo |
foo |
main |
main |
main |
main |
_start |
_start |
_start |
_start |
其中可能出现 <OMP-overhead> 的窗口相当小,所以该函数可能不出现在任何特定实验中。
在并行区域中执行时
所有四个线程都在并行区域中执行有用的工作。
主线程 |
从属线程 1 |
从属线程 2 |
从属线程 3 |
---|---|---|---|
foo-OMP... |
foo-OMP... |
foo-OMP... |
foo-OMP... |
foo |
foo |
foo |
foo |
main |
main |
main |
main |
_start |
_start |
_start |
_start |
在并行区域中工作块之间执行时
所有四个线程都在执行有用的工作,但是一个线程已完成一个工作块,并正在获取其下一个工作块。
主线程 |
从属线程 1 |
从属线程 2 |
从属线程 3 |
---|---|---|---|
<OMP-overhead> | |||
foo-OMP... |
foo-OMP... |
foo-OMP... |
foo-OMP... |
foo |
foo |
foo |
foo |
main |
main |
main |
main |
_start |
_start |
_start |
_start |
在并行区域内的临界段中执行时
所有四个线程都在执行,每个线程都在并行区域内。其中一个线程在临界段中,而其他线程之一在到达临界段之前(或完成它之后)正在运行。剩余的两个线程正在等待,以便它们自己进入临界段。
主线程 |
从属线程 1 |
从属线程 2 |
从属线程 3 |
---|---|---|---|
<OMP-critical_section_wait> |
<OMP-critical_section_wait> |
||
foo-OMP... |
foo-OMP... |
foo-OMP... |
foo-OMP... |
foo |
foo |
foo |
foo |
main |
main |
main |
main |
_start |
_start |
_start |
_start |
收集的数据不区分正在临界段中执行的线程的调用栈与尚未到达或已通过临界段的线程的调用栈。
在并行区域内的锁定周围执行时
锁定周围的代码段完全类似于临界段。所有四个线程都在并行区域内执行。一个线程在持有锁定时执行,一个在获取锁定之前(或获取并释放它之后)执行,另外两个线程正在等待锁定。
主线程 |
从属线程 1 |
从属线程 2 |
从属线程 3 |
---|---|---|---|
<OMP-lock_wait> |
<OMP-lock_wait> |
||
foo-OMP... |
foo-OMP... |
foo-OMP... |
foo-OMP... |
foo |
foo |
foo |
foo |
main |
main |
main |
main |
_start |
_start |
_start |
_start |
与临界段示例中一样,收集的数据不区分持有锁定并执行的线程的调用栈与在获取锁定之前或释放锁定之后执行的线程的调用栈。
在并行区域结尾附近
此时,其中三个线程已完成其所有工作块,但是还有一个线程仍在工作中。在这种情况下,OpenMP 构造隐式指定了屏障;如果用户代码已显式指定屏障,则 <OMP-implicit_barrier> 函数将由 <OMP-explicit_barrier> 替换。
主线程 |
从属线程 1 |
从属线程 2 |
从属线程 3 |
---|---|---|---|
<OMP-implicit_barrier> |
<OMP-implicit_barrier> |
|
<OMP-implicit_barrier> |
foo-OMP... |
foo-OMP... |
foo-OMP... |
foo-OMP... |
foo |
foo |
foo |
foo |
main |
main |
main |
main |
_start |
_start |
_start |
_start |
在并行区域结尾附近,具有一个或多个归约变量
此时,其中两个线程已完成其所有工作块,而且正在执行归约计算,但是其中一个线程仍在工作,第四个线程已完成其归约部分,正在屏障处等待。
主线程 |
从属线程 1 |
从属线程 2 |
从属线程 3 |
---|---|---|---|
<OMP-reduction> |
<OMP-implicit_barrier> |
|
<OMP-implicit_barrier> |
foo-OMP... |
foo-OMP... |
foo-OMP... |
foo-OMP... |
foo |
foo |
foo |
foo |
main |
main |
main |
main |
_start |
_start |
_start |
_start |
尽管在 <OMP-reduction> 函数中显示了一个线程,但是执行归约所用的实际时间通常相当少,且在调用栈样本中很少捕获到。
在并行区域的结尾
此时,所有线程均已完成并行区域内的所有工作块,且已到达屏障。
主线程 |
从属线程 1 |
从属线程 2 |
从属线程 3 |
---|---|---|---|
<OMP-implicit_barrier> |
<OMP-implicit_barrier> |
<OMP-implicit_barrier> |
<OMP-implicit_barrier> |
foo-OMP... |
foo-OMP... |
foo-OMP... |
foo-OMP... |
foo |
foo |
foo |
foo |
main |
main |
main |
main |
_start |
_start |
_start |
_start |
由于所有线程均已到达屏障,因此它们都可能会继续,实验不大可能找到处于此状态的所有线程。
离开并行区域之后
此时,所有从属线程都在等待进入下一个并行区域,它们处于自旋或休眠状态,具体状态取决于用户设置的各种环境变量。程序以串行方式执行。
主线程 |
从属线程 1 |
从属线程 2 |
从属线程 3 |
---|---|---|---|
foo | |||
main | |||
_start |
<OMP-idle> |
<OMP-idle> |
<OMP-idle> |
在嵌套并行区域中执行时
所有四个线程都在工作,每个线程都在外部并行区域内。其中一个从属线程已调用另一函数 bar,并已创建嵌套并行区域,而且会创建一个附加从属线程以便与它一起工作。
主线程 |
从属线程 1 |
从属线程 2 |
从属线程 3 |
从属线程 4 |
---|---|---|---|---|
bar-OMP... |
bar-OMP... |
|||
bar |
bar |
|||
foo-OMP... |
foo-OMP... |
foo-OMP... |
foo-OMP... |
foo-OMP... |
foo |
foo |
foo |
foo |
foo |
main |
main |
main |
main |
main |
_start |
_start |
_start |
_start |
_start |
处理 OpenMP 程序的时钟分析事件时,将显示两个度量,它们分别对应于 OpenMP 系统中的两种状态所用的时间。它们是“OMP 工作”和“OMP 等待”。
只要从用户代码执行线程(不管串行执行还是并行执行),就会在“OMP 工作”中累计时间。只要线程正在等待某项,之后才能继续,就会在“OMP 等待”中累计时间,而不管等待是忙等待(自旋等待)还是休眠。这两个度量的总和与时钟分析中的“总 LWP 时间”度量相匹配。
在执行的各个阶段中,程序的实际调用栈与上面在直观模型中描述的有很大差异。机器表示模式将调用栈显示为已度量,没有进行转换,且没有构造人工函数。但是,仍显示时钟分析度量。
在下面的每个调用栈中,libmtsk 表示 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。)
所有线程都在屏障处时
主线程 |
从属线程 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 |