profile使用:
profile功能是架构无关的,可以用来监视linux内核的4项功能,即:
11 #define CPU_PROFILING 1
12 #define SCHED_PROFILING 2
13 #define SLEEP_PROFILING 3
14 #define KVM_PROFILING 4
要想找开profile功能,除了要在menuconfig中打开支持选项外,还要在命令行加上profile=**,##.
**表示上述4种功能之一,##表示一个数字,用来表示监视的颗粒度,越小越细。
当做了这些工作之后还需要用到util linux工具中的readprofile来读取结果,结果是从/proc/profile文件中读取的,此工具做了格式化处理。以下为转载:
1. 如何使用profile:
首先确认内核支持profile,然后在内核启动时加入以下参数:profile=1或者其它参数, 新的内核支持profile=schedule,1
2. 内核启动后会创建/proc/profile文件,这个文件可以通过readprofile读取,
如readprofile -m /proc/kallsyms | sort -nr > ~/cur_profile.log,
或者readprofile -r -m /proc/kallsyms |sort -nr,
或者readprofile -r && sleep 1 && readprofile -m /proc/kallsyms |sort -nr >~/cur_profile.log
3. 读取/proc/profile可获得哪些内容?
根据启动配置profile=?的不同,获取的内容不同:
如果设置成profile=schedule可以获得每个函数调用schedule的次数,用来调试schedule很有用
profile的实现:
在内核中创建一个/proc/profile接口,在系统启动时用profile_init()分配好存放profile信息的内存,每条指令都有一个计数器。
如果设置的是profile=2 统计每条指令执行的次数。在时钟中断中调用 profile_tick(CPU_PROFILING, regs),将当前指令regs->eip的计数值+1。这个统计有点不准,因为一个jiffies之间,可能执行很多函数,而统计的只是恰好发生 时钟中断时的那个函数。但取样点多了,这些信息还是能说明问题。
如果设置的是profile=schedule 统计每个指令调用schedule()的次数,在schedule()中调用profile_hit(SCHED_PROFILING, __builtin_return_address(0));
其实真正调用schedule的指令只有有限的几个,但这些信息可以获得调度点的精确信息。
profile_hit()的作用是将当前指令的计数值加1
profile_tick()是在每个时钟tick的时候将响应的指令计数值加1
time_hook 一般被其它profile工具,如oprofile用来在每次中断发生时,添加自己的处理函数。
profile信息其实包括任务的所有统计信息,所以可以用profile_event_register()在任务退出或者用户空间内存释放时,挂载自己的回调函数,以统计这些信息。
profile信息的统计在smp和up下不同,即profile_hit的实现不同,smp的实现中有一个PerCPU cache,这可避免多个CPU在profile统计时效率低下问题。具体可以察看源代码kernel/profile.c
oprofile使用:
oprofile平台相关工具,请注意自己平台支持的event.
http://oprofile.sourceforge.net/doc/index.html
http://oprofile.sourceforge.net/doc/internals/index.html
简介
作为一名开发人员,在试图提高代码效率时,您可能发现性能瓶颈是您要面对的最困难的任务之一。代码分析(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 < x; i++)
z = z + y;
return z;
}
int main()
{
int i,j;
int x,y;
for (i = 0; i < 200; i ++) {
for (j = 0; j " 30 ; j++) {
x = fast_multiply(i, j);
y = slow_multiply(i, j);
}
}
return 0;
}
分析这个代码,并使用 opannotate 对其进行分析,该工具使您可以用 OProfile 注释查看源代码。首先必须利用调试信息来编译源代码,opannotate 要用它来添加注释。使用 Gnu Compiler Collections C 编译器,即 gcc,通过运行以下命令来编译清单 1 中的例子。注意,-g 标志意味着要添加调试信息。
gcc -g multiply.c -o multiply
接下来,使用 清单 2 中的命令分析该代码,然后使用 CYCLES 事件计算处理器周期,以分析结果。
清单 2. 用来分析乘法例子的命令
# opcontrol --vmlinux=/boot/vmlinux-2.6.5-7.139-pseries64
# opcontrol --reset
# opcontrol --setup --event=CYCLES:1000
# opcontrol --start
Using 2.6+ OProfile kernel interface.
Reading module info.
Using log file /var/lib/oprofile/oprofiled.log
Daemon started.
Profiler running.
# ./multiply
# opcontrol --dump
# opcontrol --stop
Stopping profiling.
# opcontrol -h
Stopping profiling.
Killing daemon.
最后,使用 opannotate 工具和 --source 选项生成源代码,或者和 --assembly 选项一起生成汇编代码。具体使用这两个选项中的哪一个选项,或者是否同时使用这两个选项,则取决于您想要分析的详细程度。对于这个例子,只需使用 --source 选项来确定大部分处理器周期发生在什么地方即可。
清单 3. 对乘法例子的 opannotate 结果的分析
# opannotate --source ./multiply
/*
* Command line: opannotate --source ./multiply
*
* Interpretation of command line:
* Output annotated source file with samples
* Output all files
*
* CPU: ppc64 POWER5, speed 1656.38 MHz (estimated)
* Counted CYCLES events (Processor cycles) with a unit mask of
0x00 (No unit mask) count 1000
*/
/*
* Total samples for file : "/usr/local/src/badcode/multiply.c"
*
* 6244 100.000
*/
:int fast_multiply(x, y)
36 0.5766 :{ /* fast_multiply total: 79 1.2652 */
26 0.4164 : return x * y;
17 0.2723 :}
:
:int slow_multiply(x, y)
50 0.8008 :{ /* slow_multiply total: 6065 97.1332 */
: int i, j, z;
2305 36.9154 : for (i = 0, z = 0; i " x; i++)
3684 59.0006 : z = z + y;
11 0.1762 : return z;
15 0.2402 :}
:
:int main()
:{ /* main total: 100 1.6015 */
: int i,j;
: int x,y;
:
1 0.0160 : for (i = 0; i " 200; i ++) {
6 0.0961 : for (j = 0; j " 30 ; j++) {
75 1.2012 : x = fast_multiply(i, j);
18 0.2883 : y = slow_multiply(i, j);
: }
: }
: return 0;
:}
清单 3 中下面的几行将显示两个乘法函数中所使用的 CYCLES 数:
36 0.5766 :{ /* fast_multiply total: 79 1.2652 */
50 0.8008 :{ /* slow_multiply total: 6065 97.1332 */
您可以看到,fast_mulitply() 只使用了 79 个样本,而 slow_multiply() 使用了 6065 个样本。虽然这是一个很小的例子,在现实中不大可能出现,但它仍然足以演示如何剖析代码,并为发现性能瓶颈而对其进行分析。
例 2:发现二级数据缓存失误
这个例子比第一个例子要复杂一些,它需要发现二级(L2)数据缓存失误。POWER 处理器包含芯片二级缓存(on-chip L2 cache),这是邻近处理器的一种高速存储器。处理器从 L2 缓存中访问经常修改的数据。当两个处理器共享一个数据结构,并同时修改那个数据结构时,就有可能引发问题。CPU1 在它的 L2 缓存中包含数据的一个副本,而 CPU2 修改了这个共享的数据结构。CPU1 L2 缓存中的副本现在是无效的,必须进行更新。CPU1 必须花费大量步骤从主存中检索数据,这需要占用额外的处理器周期。
在这个例子中,您将查看这个数据结构(如清单 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)) < 0) {
perror("clone called failed.");
exit(1);
}
/* increment first member of shared struct */
for (j = 0; j < 2000; j++) {
for (i = 0; i < 100000; i++) {
shared_data.data1++;
}
}
return 0;
}
int inc_second(struct shared_data_struct *sd)
{
int i,j;
/* increment second member of shared struct */
for (j = 1; j < 2000; j++) {
for (i = 1; i < 100000; i++) {
sd->data2++;
}
}
}
使用 gcc 编译器,运行清单 6 中的命令不带优化地编译这个示例程序。
清单 6. 用于编译清单 5 中例子代码的命令
gcc -o cache-miss cache-miss.c
现在您可以用 OProfile 分析上述程序中出现的 L2 数据缓存失误。
对于这个例子,作者在一台 IBM eServer? OpenPower? 710 上执行和分析了这个程序,该机器有两个 POWER5 处理器,并运行 SLES9 Service Pack 1 (SLES9SP1)。将 --list-events 标志传递给 opcontrol,以判断是哪一个事件负责监视 L2 数据缓存失误。对于基于 POWER5 处理器的、运行 SLES9SP1 的系统,由 PM_LSU_LMQ_LHR_MERGE_GP9 事件监视 L2 数据缓存失误。如果您将样本计数设置为 1000,比如在这个例子中,那么 OProfile 将从每 1000 个硬件事件抽取一个样本。如果使用不同的平台,例如基于 POWER4 处理器的服务器,那么这样的事件也会有所不同。
使用 清单 7 中的命令分析这个例子代码,如下所示:
清单 7. 用来分析清单 5 所示例子中的 L2 数据缓存失误的命令
# opcontrol --vmlinux=/boot/vmlinux-2.6.5-7.139-pseries64
# opcontrol --reset
# opcontrol --setup –event=PM_LSU_LMQ_LHR_MERGE_GP9:1000
# opcontrol --start
Using 2.6+ OProfile kernel interface.
Reading module info.
Using log file /var/lib/oprofile/oprofiled.log
Daemon started.
Profiler running.
# ./cache-miss
# opcontrol --dump
# opcontrol -h
Stopping profiling.
Killing daemon.
# opreport -l ./cache-miss
CPU: ppc64 POWER5, speed 1656.38 MHz (estimated)
Counted PM_LSU_LMQ_LHR_MERGE_GP9 events (Dcache miss occurred for
the same real cache line as earlier req, merged into LMQ) with a
unit mask of 0x00 (No unit mask) count 1000
samples % symbol name
47897 58.7470 main
33634 41.2530 inc_second
在分析来自 opreport 的结果时,您可以看到,在函数 main() 和 inc_second() 中存在很多缓存失误。opreport 的 -l 选项将输出符号信息,而实质上输出的应该只是二进制映像名。同样,缓存失误的起因也是两个处理器修改一个共享的数据结构,这个数据结构大小为 8 字节,放在一个 128 字节的缓存行中。
消除数据缓存失误的一种方法是填充数据结构,使得它的每一个元素都存储在各自的缓存行中。清单 8 包含一个修改后的结构,其中有 124 字节的填充物。
清单 8. 带填充物的数据结构,每个元素放进不同的缓存行中
struct shared_data_struct {
unsigned int data1;
char pad[124];
unsigned int data1;
像前面那样重新编译该程序,但是这一次使用修改后的数据结构。然后使用 清单 9 中的命令再次分析结果。
清单 9. 填充数据结构后用于 profile L2 数据缓存失误的命令
# opcontrol --vmlinux=/boot/vmlinux-2.6.5-7.139-pseries64
# opcontrol --reset
# opcontrol --setup –event=PM_LSU_LMQ_LHR_MERGE_GP9:1000
# opcontrol --start
Using 2.6+ OProfile kernel interface.
Reading module info.
Using log file /var/lib/oprofile/oprofiled.log
Daemon started.
Profiler running.
# ./cache-miss
# opcontrol --dump
# opcontrol -h
Stopping profiling.
Killing daemon.
# opreport -l ./cache-miss
error: no sample files found: profile specification too strict ?
Opreport 表明,由于没有发现抽样数据,所以可能存在错误。然而,随着对共享数据结构的修改,这是可以预期的,因为每个数据元素都在自己的缓存行中,所以不存在 L2 缓存失误。
现在可以考察 L2 缓存失误在处理器周期上的代价。首先,分析使用未填充的原有共享数据结构的代码(清单 4)。您将进行抽样的事件是 CYCLES。使用 清单 10 中的命令针对 CYCLES 事件分析这个例子。
清单 10. 用于 profile 清单 5 所示例子中处理器周期数的命令
# opcontrol --vmlinux=/boot/vmlinux-2.6.5-7.139-pseries64
# opcontrol --reset
# opcontrol --setup –event=CYCLES:1000
# opcontrol --start
Using 2.6+ OProfile kernel interface.
Reading module info.
Using log file /var/lib/oprofile/oprofiled.log
Daemon started.
Profiler running.
# ./cache-miss
# opcontrol --dump
# opcontrol -h
Stopping profiling.
Killing daemon.
# opreport -l ./cache-miss
CPU: ppc64 POWER5, speed 1656.38 MHz (estimated)
Counted CYCLES events (Processor cycles) with a unit mask of 0x00
(No unit mask) count 1000
samples % symbol name
121166 53.3853 inc_second
105799 46.6147 main
现在,使用 清单 11 中的命令分析使用填充后的数据结构的例子代码(清单 8)。
清单 11. 用于分析使用填充后的数据结构的例子中处理器周期数的命令
# opcontrol --vmlinux=/boot/vmlinux-2.6.5-7.139-pseries64
# opcontrol --reset
# opcontrol --setup –event=CYCLES:1000
# opcontrol --start
Using 2.6+ OProfile kernel interface.
Reading module info.
Using log file /var/lib/oprofile/oprofiled.log
Daemon started.
Profiler running.
# ./cache-miss
# opcontrol --dump
# opcontrol -h
Stopping profiling.
Killing daemon.
# opreport -l ./cache-miss
CPU: ppc64 POWER5, speed 1656.38 MHz (estimated)
Counted CYCLES events (Processor cycles) with a unit mask of 0x00
(No unit mask) count 1000
samples % symbol name
104916 58.3872 inc_second
74774 41.6128 main
不出所料,随着 L2 缓存失误数量的增加,处理器周期数也有所增加。其主要原因是,与从 L2 缓存取数据相比,从主存获取数据代价昂贵。
避免两个处理器之间缓存失误的另一种方法是在相同处理器上运行两个线程。通过使用 Cpu 相似性(affinity),将一个进程绑定到一个特定的处理器,下面的例子演示了这一点。在 Linux 上,sched_setaffinity() 系统调用在一个处理器上运行两个线程。 清单 12 提供了原来的示例程序的另一个变体,其中使用 sched_setaffinity() 调用来执行这一操作。
清单 12. 利用 cpu 相似性来避免 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;
cpu_set_t cmask;
unsigned long len = sizeof(cmask);
pid_t p = 0;
void *child_stack;
__CPU_ZERO(&cmask);
__CPU_SET(0, &cmask);
/* 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)) < 0) {
perror("clone called failed");
exit(1);
}
if (!sched_setaffinity(0, len, &cmask)) {
printf("Could not set cpu affinity for current
process.\n");
exit(1);
}
if (!sched_setaffinity(pid, len, &cmask)) {
printf("Could not set cpu affinity for cloned
process.\n");
exit(1);
}
/* increment first member of shared struct */
for (j = 0; j < 2000; j++) {
for (i = 0; i < 100000; i++) {
shared_data.data1++;
}
}
return 0;
}
int inc_second(struct shared_data_struct *sd)
{
int i,j;
/* increment second member of shared struct */
for (j = 1; j < 2000; j++) {
for (i = 1; i < 100000; i++) {
sd->data2++;
}
}
}
这个例子在同处理器上运行两个线程,共享数据结构存放在一个处理器上的一个 L2 缓存行中。这样应该可以导致零缓存失误。使用前面描述的步骤分析缓存失误,以验证在一个处理器上运行两个进程时,是否不存在 L2 缓存失误。对于数据缓存失误这个问题,第三种解决方法是使用编译器优化,这样可以减少缓存失误的数量。然而,在某些环境下,这不是一个合适的选择,您仍然必须分析代码,并对不良性能做出改正。
结束语
分析是开发过程中最困难的任务之一。为了使代码获得最佳性能,好的工具是必不可少的。OProfile 就是这样一种工具,目前它提供了针对 Linux on POWER 的分析功能。对于其他平台上的可以快速移植到 Linux on POWER 的 Linux,还有其他许多性能和调试工具。除了处理器事件的类型有所差别外,在基于 POWER 处理器的 Linux 平台上运行 OProfile 与在其他架构上运行 OProfile 是类似的。所以,如果在其他平台上使用过 OProfile,那么您应该在很短时间内就可以知道如何在 Linux on POWER 上运行 OProfile。