调用栈是一系列程序计数器 (program counter, PC) 地址,表示来自程序内的指令。第一个 PC 称为叶 PC,它位于堆栈的底部,是要执行的下一条指令的地址。下一个 PC 是对包含叶 PC 的函数的调用的地址;下一个 PC 是对该函数的调用的地址,依此类推,直至到达堆栈的顶部。每个这样的地址称为返回地址。记录调用栈的过程涉及从程序栈获取返回地址,这称为展开堆栈。有关展开失败的信息,请参见不完全的堆栈展开。
调用栈中的叶 PC 用于将独占度量从性能数据分配到该 PC 所在的函数。堆栈中的每个 PC(包括叶 PC)用于将包含度量分配到它所在的函数。
大多数时候,已记录调用栈中的 PC 自然地对应于出现在程序源代码中的函数,而且性能分析器的已报告度量直接对应于这些函数。但是,有时程序的实际执行并不与有关程序如何执行的简单直观模型对应,而且性能分析器的已报告度量可能会引起混淆。有关此类情况的更多信息,请参见将地址映射到程序结构。
程序执行的最简单案例是单线程程序调用它自己的装入对象内的函数。
将程序装入到内存中开始执行时,会为其建立上下文,包括要执行的初始地址、初始寄存器集和堆栈(用于存储临时数据和用于跟踪函数如何相互调用的内存区域)。初始地址始终位于函数 _start()(它内置于每个可执行程序中) 的开头。
程序运行时,将按顺序执行指令,直至遇到分支指令,该指令以及其他指令可能表示函数调用或条件语句。在分支点上,控制权转移到分支目标指定的地址,然后从该地址继续执行。(通常,已提交分支后的下一条指令以供执行:此指令称为分支延迟槽指令。但是,有些分支指令会取消分支延迟槽指令的执行。)
当执行表示调用的指令序列时,返回地址被放入寄存器,且在被调用函数的第一条指令处继续执行。
在大多数情况下,在被调用函数的前几个指令中的某个位置,一个新帧(用于存储有关函数的信息的内存区域)会被推到堆栈上,而返回地址被放入该帧。然后,在被调用函数本身调用其他函数时可以使用用于返回地址的寄存器。函数即将返回时,将其帧从堆栈中弹出,而且控制权返回到从其调用该函数的地址。
一个共享对象中的函数调用另一个共享对象中的函数时,其执行情况比在程序内对函数的简单调用更复杂。每个共享对象都包含一个程序链接表 (Program Linkage Table, PLT),该表包含位于该共享对象外部并从该共享对象引用的每个函数的条目。最初,PLT 中每个外部函数的地址实际上是 ld.so(即动态链接程序)内的地址。第一次调用这样的函数时,控制权将转移到动态链接程序,该动态链接程序会解析对实际外部函数的调用并为后续调用修补 PLT 地址。
如果在执行三个 PLT 指令之一的过程中发生分析事件,则 PLT PC会被删除,并将独占时间归属到调用指令。如果在首次通过 PLT 条目调用过程中发生分析事件,但是叶 PC 不是 PLT 指令之一,则 PLT 和 ld.so 中的代码引起的任何 PC 都将由对人工函数 @plt 的调用替换,该函数将累计包含时间。每个共享对象都有一个这样的人工函数。如果程序使用 LD_AUDIT 接口,则可能从不修补 PLT 条目,而且来自 @plt 的非叶 PC 可能发生得更频繁。
将信号发送到进程时,会发生各种寄存器和堆栈操作,使得发送信号时的叶 PC 看起来好像是对系统函数 sigacthandler() 的调用的返回地址。sigacthandler() 调用用户指定的信号处理程序,就像任何函数调用另一个函数一样。
性能分析器将信号传送产生的帧视为普通帧。传送信号时的用户代码显示为调用系统函数 sigacthandler(),而 sigacthandler() 又显示为调用用户的信号处理程序。来自 sigacthandler() 和任何用户信号处理程序以及它们调用的任何其他函数的包含度量,都显示为中断函数的包含度量。
收集器通过插入 sigaction(),以确保其处理程序在收集时钟数据时是 SIGPROF 信号的主处理程序,而在收集硬件计数器溢出数据时是 SIGEMT 信号的主处理程序。
陷阱可以由指令或硬件发出,而且由陷阱处理程序捕获。系统陷阱是指通过指令启动的陷阱,它们会陷入内核。所有系统调用均使用陷阱指令实现。硬件陷阱的一些示例是浮点单元无法完成某个指令(例如,用于 UltraSPARC® III 平台上的某些寄存器内容值的 fitos 指令)时发出的陷阱,或者指令没有在硬件中实现时发出的陷阱。
发出陷阱时,Solaris LWP 或 Linux 内核进入系统模式。在 Solaris OS 上,微态通常从用户 CPU 状态切换到陷阱状态,再切换到系统状态。处理陷阱所用的时间可以显示为系统 CPU 时间和用户 CPU 时间的组合,具体取决于切换微态的时间点。该时间被归属到用户代码中从其启动陷阱的指令(或归属到系统调用)。
对于某些系统调用,提供尽可能高效的调用处理被认为是很关键的。由这些调用生成的陷阱称为快速陷阱。生成快速陷阱的系统函数包括 gethrtime 和 gethrvtime。在这些函数中,由于涉及到的开销,所以不会切换微态。
在其他情况下,提供尽可能高效的陷阱处理也被认为是很关键的。其中的一些示例是 TLB(translation lookaside buffer,转换后备缓冲器)未命中以及寄存器窗口溢出和填充,其中不切换微态。
在这两种情况下,所用的时间都记录为用户 CPU 时间。但是,由于 CPU 模式已切换为系统模式,所以将关闭硬件计数器。因此,通过求出用户 CPU 时间和周期时间(最好在同一实验中记录)之间的差值,可以估算处理这些陷阱所用的时间。
有一种陷阱处理程序切换回用户模式的情况,那是 Fortran 中在 4 字节边界上对齐的 8 字节整数的未对齐内存引用陷阱。陷阱处理程序的帧出现在堆栈上,而对处理程序的调用可以出现在性能分析器中,归属到整数装入或存储指令。
指令陷入内核后,陷阱指令后的指令看起来要使用很长时间,这是因为它在内核完成陷阱指令的执行之前无法启动。
只要特定函数执行的最后一个操作是调用另一个函数,编译器就可以执行一种特定的优化。被调用者可重用来自调用者的帧,而不是生成新的帧,而且可从调用者复制被调用者的返回地址。此优化的动机是减小堆栈的大小,以及(在 SPARC 平台上)减少对寄存器窗口的使用。
假定程序源代码中的调用序列与如下所示类似:
A -> B -> C -> D
对 B 和 C 进行尾部调用优化后,调用栈看起来好像是函数 A 直接调用函数 B、C 和 D。
A -> B A -> C A -> D
也就是说,调用树被展平。使用 -g 选项编译代码时,尾部调用优化仅发生在编译器优化级别 4 或更高级别上。在不使用 -g 选项的情况下编译代码时,尾部调用优化发生在编译器优化级别 2 或更高级别上。
在 Solaris OS 中,简单程序在单个 LWP(lightweight process,轻量级进程)上的单个线程中执行。多线程可执行程序调用线程创建函数(执行的目标函数会传递到该函数)。目标退出时,会销毁线程。
Solaris OS 支持两种线程实现:Solaris 线程和 POSIX 线程 (Pthread)。从 Solaris 10 OS 开始,这两种线程实现都包括在 libc.so 中。在 Solaris 9 OS 中,线程实现包含在单独的库(即 libthread.so 和 libpthread.so)中。
对于 Solaris 线程,新创建的线程从名为 _thread_start() 的函数开始执行,该函数调用在线程创建调用中传递的函数。对于涉及目标由此线程执行的任何调用栈,堆栈的顶部是 _thread_start(),与线程创建函数的调用者没有任何联系。因此,与所创建的线程关联的包含度量仅传播至 _thread_start() 和 <Total> 函数。除了创建线程外,Solaris 线程实现还在 Solaris 上创建 LWP 以执行线程。每个线程都绑定到特定的 LWP。
Pthread 在 Solaris 10 OS 中以及 Linux OS 中可用于显式多线程。
在这两种环境中,为创建新线程,应用程序会调用 Pthread API 函数 pthread_create(),将指针作为函数参数之一传递到应用程序定义的启动例程。
在 Solaris OS 上,新的 pthread 开始执行时,将会调用 _lwp_start() 函数。在 Solaris 10 OS 上,_lwp_start() 调用中间函数 _thr_setup(),该中间函数随后调用在 pthread_create() 中指定的应用程序定义的启动例程。在 Solaris 9 OS 上,_lwp_start() 直接调用应用程序的启动例程。
在 Linux OS 上,新的 pthread 开始执行时,将会运行 Linux 特定的系统函数 clone(),该系统函数调用另一个内部初始化函数 pthread_start_thread(),该初始化函数又调用在 pthread_create() 中指定的应用程序定义的启动例程。可用于收集器的 Linux 度量收集函数是线程特定的。因此,collect 实用程序运行时,会在 pthread_start_thread() 和应用程序定义的线程启动例程之间插入一个名为 collector_root() 的度量收集函数。
对于典型的开发者,基于 Java 技术的应用程序就像任何其他程序那样运行。此类应用程序从主入口点(通常名为 class.main,可以调用其他方法)开始,就像 C 或 C++ 应用程序那样。
对于操作系统,使用 Java 编程语言(纯 Java 或与 C/C++ 混合)编写的应用程序作为实例化 JVM 软件的进程运行。JVM 软件是从 C++ 源代码编译的,从 _start(它会调用 main 等)开始执行。它从 .class 和/或 .jar 文件读取字节码,并执行在该程序中指定的操作。可以指定的操作包括动态装入本机共享对象以及调用该对象内包含的各种函数或方法。
JVM 软件可以执行许多用传统语言编写的应用程序通常不能执行的操作。启动时,该软件会在其数据空间中创建许多动态生成的代码的区域。其中一个区域是用于处理应用程序的字节码方法的实际解释器代码。
在执行基于 Java 技术的应用程序期间,JVM 软件解释大多数方法;这些方法称为已解释的方法。Java HotSpot 虚拟机会在解释字节码以检测频繁执行的方法时监视性能。然后,Java HotSpot 虚拟机可能编译重复执行的方法,以生成这些方法的机器码。生成的方法称为已编译的方法。之后,虚拟机执行更高效的已编译方法,而不是解释方法的原始字节码。已编译的方法会被装入应用程序的数据空间,并且可能会在之后的某个时间点卸载它们。此外,还会在数据空间中生成其他代码以执行已解释代码和已编译代码之间的转换。
用 Java 编程语言编写的代码还可以直接调用本机编译的代码(C、C++ 或 Fortran);此类调用的目标称为本机方法。
用 Java 编程语言编写的应用程序本身就是多线程的,对于用户程序中的每个线程,都具有一个 JVM 软件线程。Java 应用程序还具有若干个内务处理线程,用于信号处理、内存管理和 Java HotSpot 虚拟机编译。
在 J2SE 5.0 中的 JVMTI 上,可通过各种方法实现数据收集。
性能工具通过记录每个 Solaris LWP 或 Linux 线程生存期中的事件,以及发生事件时的调用栈来收集其数据。在执行任何应用程序的任意点上,调用栈表示程序在其执行中所处的位置以及它如何到达该位置。混合模型 Java 应用程序区别于传统 C、C++ 和 Fortran 应用程序的一个重要方面是,在运行目标的过程中的任何瞬间都存在两个有意义的调用栈:Java 调用栈和机器调用栈。这两个调用栈都在配置期间进行记录,并在分析期间进行协调。
用于 Java 程序的基于时钟的分析和硬件计数器溢出分析的工作方式与用于 C、C++ 和 Fortran 程序的情况基本相同,不同之处在于前者会同时收集 Java 调用栈和机器调用栈。
Java 程序的同步跟踪基于线程尝试获取 Java 监视器时生成的事件。将会为这些事件同时收集机器调用栈和 Java 调用栈,但不为在 JVM 软件中使用的内部锁收集同步跟踪数据。
堆跟踪数据记录由用户代码生成的对象分配事件以及由垃圾收集器生成的对象解除分配事件。此外,对 C/C++ 内存管理函数(如 malloc 和 free)的任何使用也将生成记录的事件。
对于用 Java 编程语言编写的应用程序,有以下三种显示性能数据的表示法:Java 表示法、专家 Java 表示法和机器表示法。缺省情况下,将显示 Java 表示法(前提是数据支持它)。下一节汇总了这三种表示法的主要差异。
用户表示法按名称显示已编译的和已解释的 Java 方法,并以其自然形式显示本机方法。在执行过程中,可能存在已执行的特定 Java 方法的许多实例:已解释的版本,也许还有一个或多个已编译的版本。在 Java 表示法中,所有方法会被聚集显示为一个方法。缺省情况下,在分析器中选定此表示法。
Java 表示法中 Java 方法的 PC 与该方法中的方法 id 和字节码索引相对应;本机函数的 PC 与机器 PC 相对应。Java 线程的调用栈可能同时具有 Java PC 和机器 PC。它没有对应于 Java 内务处理代码(无 Java 表示法)的任何帧。在某些情况下,JVM 软件无法展开 Java 堆栈,将返回单个帧及特殊函数 <no Java callstack recorded>。通常,它占总时间的比例不会超过 5-10%。
Java 表示法中的函数列表针对所调用的 Java 方法和任何本机方法显示度量。调用者-被调用者面板显示 Java 表示法中的调用关系。
Java 方法的源代码对应于 .java 文件(从中编译源代码,每个源代码行上都有度量)中的源代码。任何 Java 方法的反汇编显示为它生成的字节码,以及针对每个字节码的度量和交错的 Java 源代码(如果可用)。
Java 表示法中的时间线仅显示 Java 线程。每个线程的调用栈与其 Java 方法一起显示。
所有 Java 程序都可能具有显式同步,通常是通过调用 monitor-enter 例程执行的。
Java 表示法中的同步延迟跟踪基于 JVMTI 同步事件。Java 表示法中不显示来自常规同步跟踪的数据。
当前不支持 Java 表示法中的数据空间分析。
专家 Java 表示法与 Java 表示法类似,不同之处是在专家 Java 表示法中公开了在 Java 表示法中抑制的一些 JVM 内部详细信息。对于专家 Java 表示法,时间线显示所有线程;内务处理线程的调用栈是本机调用栈。
机器表示法显示来自 JVM 软件本身而不是来自 JVM 软件解释的应用程序的函数。该表示法还显示所有已编译方法和本机方法。机器表示法看起来与用传统语言编写的应用程序的表示法相同。调用栈显示 JVM 帧、本地帧和编译方法帧。一些 JVM 帧表示已解释的 Java、已编译的 Java 和本机代码之间的转换代码。
针对 Java 源代码显示已编译方法的源代码;数据表示所选已编译方法的特定实例。已编译方法的反汇编显示生成的机器汇编程序代码,而不是 Java 字节码。调用者-被调用者关系显示所有开销帧,以及表示已解释方法、已编译方法和本机方法之间的转换的所有帧。
机器表示法中的时间线以条形图显示所有线程、LWP 或 CPU,而其中每项的调用栈都是机器表示法调用栈。
在机器表示法中,线程同步被移交给对 _lwp_mutex_lock 的调用。不显示同步数据,因为未跟踪这些调用。
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 |
堆栈展开可能由于多种原因而失败:
如果堆栈已被用户代码破坏;如果是这样,则程序可能进行核心转储,或者数据收集代码可能进行核心转储,具体取决于堆栈被破坏的确切方式。
如果用户代码不遵循函数调用的标准 ABI 约定。特别是,在 SPARC 平台上,如果在执行保存指令之前更改了返回寄存器 %o7。
在任何平台上,手工编写的汇编程序代码都可能违反约定。
如果在从堆栈中弹出被调用者的帧之后,但在函数返回之前,叶 PC 位于函数中。
如果调用栈包含的帧超过 250 个,则收集器没有用于完全展开调用栈的空间。在这种情况下,调用栈中从 _start 到某个点的函数的 PC 不会记录在实验中。人工函数 <Truncated-stack> 显示为从 <Total> 调用,以清点所记录的最上面的帧。
如果使用 -E 或 -P 编译器选项生成中间文件,则分析器将中间文件用于带注释的源代码,而不是原始源文件。使用 -E 生成的 #line 指令可能会导致为源代码行分配度量时出现问题。
如果函数中的指令没有行号(这些行号引用为生成该函数而编译的源文件),则带注释的源代码中会出现以下行:
function_name -- <instructions without line numbers>
在以下情况下可能缺少行号:
编译时未指定 -g 选项。
调试信息在编译后被剥离,或者包含该信息的可执行文件或目标文件被移动或删除或者随后被修改。
函数包含从 #include 文件而不是从原始源文件生成的代码。
在进行较高级别优化时,如果代码从不同文件中的函数内联。
源文件具有引用某个其他文件的 #line 指令;使用 -E 选项进行编译,然后再编译生成的 .i 文件是出现此情况的一种方式。使用 -P 标志编译时也可能会出现此情况。
找不到读取行号信息的目标文件。
所用的编译器生成不完整的行号表。