使用 OProfile for Linux on POWER 识别性能瓶颈
使用 OProfile for Linux on POWER 识别性能瓶颈
2011年06月16日
使用 OProfile for Linux on POWER 识别性能瓶颈
作为一名开发人员,在试图提高代码效率时,您可能发现性能瓶颈是您要面对的最困难的任务之一。代码分析(code profiling)是一种可以使这项任务变得更容易的方法。代码分析包括对那些表示运行系统上的某些处理器活动的数据样本进行分析。OProfile 为 POWER 上的 Linux 提供了这种解决方案。OProfile 被包含在最新的 IBM?? 支持的 Linux for POWER 发行版本中:Red Hat Enterprise Linux 4 (RHEL4) 和 SUSE LINUX Enterprise Server 9 (SLES9)。本文将介绍 OProfile for Linux on POWER,并提供两个例子,演示如何使用它来发现性能瓶颈。
回页首
代码分析概述
OProfile for Linux on POWER 使用了一个内核模块和一个用户空间守护进程,前者可以访问性能计数寄存器,后者在后台运行,负责从这些寄存器中收集数据。在启动守护进程之前,OProfile 将配置事件类型以及每种事件的样本计数(sample count)。如果没有配置任何事件,那么 OProfile 将使用 Linux on POWER 上的默认事件,即 CYCLES,该事件将对处理器循环进行计数。事件的样本计数将决定事件每发生多少次计数器才增加一次。OProfile 被设计成可以在低开销下运行,从而使后台运行的守护进程不会扰乱系统性能。
OProfile 具有对 POWER4??、POWER5?? 和 PowerPC?? 970 处理器的内核支持。PowerPC 970 和 POWER4 处理器有 8 个计数寄存器,而 POWER5 处理器有 6 个计数寄存器。在不具备 OProfile 内核支持的架构上使用的则是计时器(timer)模式。在这种模式下,OProfile 使用了一个计数器中断,对于禁用中断的代码,OProfile 不能对其进行分析。
回页首
OProfile 工具
与 OProfile 内核支持一起提供的还有一些与内核交互的用户空间工具,以及分析收集到的数据的工具。如前所述,OProfile 守护进程收集样本数据。控制该守护进程的工具称作 opcontrol。表 1 列出了用于 opcontrol 的一些常见的命令行选项。本文的后面还将描述 opreport 和 opannotate 这两个工具,它们都是用于分析收集到的数据的工具。在 OProfile 手册的第 2.2 节中,可以找到对所有 OProfile 工具的概述。(请参阅参考资料。)
RHEL4 和 SLES9 上支持的处理器事件类型是不同的,正如不同 POWER 处理器上支持的事件类型也会有所变化一样。您可以使用 opcontrol 工具和 --list-events 选项获得自己平台所支持的那些事件的列表。
表 1. opcontrol 命令行选项
opcontrol 选项 描述
--list-events 列出处理器事件和单元屏蔽(unit mask)
--vmlinux= 将要分析的内核镜像文件
--no-vmlinux 不分析内核
--reset 清除当前会话中的数据
--setup 在运行守护进程之前对其进行设置
--event= 监视给定的处理器事件
--start 开始取样
--dump 使数据流到守护进程中
--stop 停止数据取样
-h 关闭守护进程
回页首
OProfile 例子
您可以使用 OProfile 来分析处理器周期、TLB 失误、内存引用、分支预测失误、缓存失误、中断处理程序,等等。同样,您可以使用 opcontrol 的 --list-events 选项来提供完整的特定处理器上可监视事件列表。
下面的例子演示了如何使用 OProfile for Linux on POWER。第一个例子监视处理器周期,以发现编写不当、会导致潜在性能瓶颈的算法。虽然这是一个很小的例子,但是当您分析一个应用程序,期望发现大部分处理器周期究竟用在什么地方时,仍可以借鉴这里的方法。然后您可以进一步分析这部分代码,看是否可以对其进行优化。
第二个例子要更为复杂一些 -- 它演示了如何发现二级(level 2,L2)数据缓存失误,并为减少数据缓存失误的次数提供了两套解决方案。
例 1: 分析编写不当的代码
这个例子的目的是展示如何编译和分析一个编写不当的代码示例,以分析哪个函数性能不佳。这是一个很小的例子,只包含两个函数 -- slow_multiply() 和 fast_multiply() -- 这两个函数都是用于求两个数的乘积,如下面的清单 1 所示。
清单 1. 两个执行乘法的函数
int fast_multiply(x, y)
{
return x * y;
}
int slow_multiply(x, y)
{
int i, j, z;
for (i = 0, z = 0; i
在这个例子中,您将查看这个数据结构(如清单 4 所示),并分析两个处理器同时修改这个数据结构时出现的情景)。然后观察数据缓存失误,并考察用来修正这个问题的两种解决方案。
清单 4. 共享的数据结构
struct shared_data_struct {
unsigned int data1;
unsigned int data1;
}
清单 5 中的程序使用 clone() 系统调用和 VM_CLONE 标志生成一个子进程。VM_CLONE 标志会导致子进程和父进程在同一个存储空间中运行。父线程修改该数据结构的第一个元素,而子线程则修改第二个元素。
清单 5. 演示 L2 数据缓存失误的代码示例
#include
#include
struct shared_data_struct {
unsigned int data1;
unsigned int data2;
};
struct shared_data_struct shared_data;
static int inc_second(struct shared_data_struct *);
int main(){
int i, j, pid;
void *child_stack;
/* allocate memory for other process to execute in */
if((child_stack = (void *) malloc(4096)) == NULL) {
perror("Cannot allocate stack for child");
exit(1);
}
/* clone process and run in the same memory space */
if ((pid = clone((void *)&inc_second, child_stack, CLONE_VM, &shared_data)) data2++;
}
}
}
使用 gcc 编译器,运行清单 6 中的命令不带优化地编译这个示例程序。
清单 6. 用于编译清单 5 中例子代码的命令
gcc -o cache-miss cache-miss.c
现在您可以用 OProfile 分析上述程序中出现的 L2 数据缓存失误。
对于这个例子,作者在一台 IBM eServe