获取性能分析数据需要以下三个步骤:
- 编译并链接您的程序,启用性能分析功能。
- 执行您的代码生成性能分析数据。
- 使用 gprof 来分析并显示性能数据。
在我们详细描述这三个步骤之前,我们将介绍一个足够规模的程序来引起对性能分析的兴趣。
一个简单的计算器
为了展示性能分析过程,我们将使用一个简单计算器程序作为案例。为了确保这个计算器占用足够的计算时间, 我们将使用在我们的真实世界程序中绝对不会用到的一元数来计算。这个程序的代码在本章的最后一节有详细说明。一元数 N 表示为 N 个相同符号的反复。比如说,数字 1 被 x 所代替,2 被 xx所替代,3 被 xxx 所代替。在我们的程序中,我们用包含 N 个元素的链表代替 N 个 x 表示一个值为 N 的一元非负数。number.c 文件包含了创建数 0 的程序,对一个数加 1,对一个数减 1,以及加、减、乘以一个数等操作。另有一个函数将一个表示非负小数的字符串转换为一元数,以及一个函数转换一个一元数为一个 int 类型的数。加法是通过不停的重复加 1 而实现的,减法则是通过不停的重复减 1 而实现的。乘数被定义为不停地重复加法。一元操作符 even 和 odd 判断传入的一元数为奇数(或偶数,与操作符相应)时返回 1,否则返回 0。这两个函数间接递归(mutually recursive)。举例来说,一个数如果为 0,或者比这个数小一的数是奇数,则这个数为偶数。
这个计算机接受一行后缀表达式1并打印出每个表达式的值。例子如下:
% ./calculator Please enter a postfix expression: 2 3 + 5 Please enter a postfix expression: 2 3 + 4 - 1
在 calculator.c 中定义的计算器会读取每一个表达式,利用在 stack.c 中定义用于存储一元数的栈存储计算的中间结果。这个栈在一个链表中存储所有一元数。
收集档案信息
为一个程序进行性能分析的第一步是为可执行文件添加手机性能信息的指令。在编译和链接对象文件时使用 -pg 编译器选项即可。参考以下例子进行编译:
% gcc -pg -c -o calculator.o calculator.c % gcc -pg -c -o stack.o stack.c % gcc -pg -c -o number.o number.c % gcc -pg calculator.o stack.o number.o -o calculator
这样编译的程序会收集函数调用和时间信息。为了逐行收集档案信息需要同时指定编译器选项 -g。如果要基本执行块的执行进行计数,例如 do-while 循环的次数等,使用 -a 选项。
第二步是运行程序。当程序开始运行时,性能分析数据被收集保存到一个 gmon.out 文件中。这些性能分析数据仅来自于被实际执行到的代码片段。您必须改变程序的输入值或者命令行参数以执行希望被分析的代码段。只有程序正常退出的情况下,性能数据才会被正确写入 gmon.out 文件。
显示性能数据
给定一个可执行文件名,gprof 分析 gmon.out 文件并显示每个函数占用的时间。举例来说,考虑使用我们的计算器计算 1787 * 13 – 1918 所得的普通(flat)档案数据,执行 gprof ./calculator 会得到:
Flat profile: Each sample counts as 0.01 seconds. % cumulative self self total time seconds seconds calls ms/call ms/call name 26.07 1.76 1.76 20795463 0.00 0.00 decrement_number 24.44 3.41 1.65 1787 0.92 1.72 add 19.85 4.75 1.34 62413059 0.00 0.00 zerop 15.11 5.77 1.02 1792 0.57 2.05 destroy_number 14.37 6.74 0.97 20795463 0.00 0.00 add_one 0.15 6.75 0.01 1788 0.01 0.01 copy_number 0.00 6.75 0.00 1792 0.00 0.00 make_zero 0.00 6.75 0.00 11 0.00 0.00 empty_stack
计算得出结论,函数 decrement_number 和它调用的所有函数占用整个执行过程 26.07% 的时间。它被调用了20,795,463 次。每一次执行需要 0.0 秒——一个短到无法测量的时间。add 函数被调用了 1,787 次,重复调用可能是为了计算乘积。每一次调用需要 0.92 秒。copy_number 函数仅被调用了 1,788 次,而它和它调用的函数仅占用总执行时间的 0.15%。在某些情况下,性能分析用的 mount 和 profile 函数会出现在列表中。
除了名为 flat profile data、用于展示各个函数占用总时间的格式之外,gprof 支持生成调用图数据(call graph data)来显示每个函数及所调用函数链所花费的总时间:
index % time self children called name <spontaneous> [1] 100.0 0.00 6.75 main [1] 0.00 6.75 2/2 apply_binary_function [2] 0.00 0.00 1/1792 destroy_number [4] 0.00 0.00 1/1 number_to_unsigned_int [10] 0.00 0.00 3/3 string_to_number [12] 0.00 0.00 3/5 push_stack [16] 0.00 0.00 1/1 create_stack [18] 0.00 0.00 1/11 empty_stack [14] 0.00 0.00 1/5 pop_stack [15] 0.00 0.00 1/1 clear_stack [17] ----------------------------------------------- 0.00 6.75 2/2 main [1] [2] 100.0 0.00 6.75 2 apply_binary_function [2] 0.00 6.74 1/1 product [3] 0.00 0.01 4/1792 destroy_number [4] 0.00 0.00 1/1 subtract [11] 0.00 0.00 4/11 empty_stack [14] 0.00 0.00 4/5 pop_stack [15] 0.00 0.00 2/5 push_stack [16] ----------------------------------------------- 0.00 6.74 1/1 apply_binary_function [2] [3] 99.8 0.00 6.74 1 product [3] 1.02 2.65 1787/1792 destroy_number [4] 1.65 1.43 1787/1787 add [5] 0.00 0.00 1788/62413059 zerop [7] 0.00 0.00 1/1792 make_zero [13]
第一段显示执行 main 和它的子函数占用了 6.75 秒程序执行时间中的 100%。它调用 apply_binary_function 函数两次,也是这个函数一共被调用两次的全部。调用者是 <spontaneous>; 这暗示性能分析器不能确定谁调用了 main。第一段同时也显示 string_to_number 调用 push_stack 函数 3 次,但后者在整个程序中总共被调用了 5 次。第三段显示了 product 函数及其调用的其它函数占用了总执行时间的 99.8%。它只被 apply_binary_function 调用了一次。
这个调用图数据显示了花费在执行一个函数和它的子函数的总时间。如果函数调用图形是一个树,这个总和将会十分容易计算,但是递归定义函数必须被特殊对待。举例来说,even 函数调用了 odd,而 odd 又调用了 even。每一个最大循环调用周期都被单独标记了一个数值,并单独显示了时间数据。再看看这个判断 1787 * 13 * 3 是不是偶数的计算过程的性能数据:
----------------------------------------------- 0.00 0.02 1/1 main [1] [9] 0.1 0.00 0.02 1 apply_unary_function [9] 0.01 0.00 1/1 even <cycle 1> [13] 0.00 0.00 1/1806 destroy_number [5] 0.00 0.00 1/13 empty_stack [17] 0.00 0.00 1/6 pop_stack [18] 0.00 0.00 1/6 push_stack [19] ----------------------------------------------- [10] 0.1 0.01 0.00 1+69693 <cycle 1 as a whole> [10] 0.00 0.00 34847 even <cycle 1> [13] ----------------------------------------------- 34847 even <cycle 1> [13] [11] 0.1 0.01 0.00 34847 odd <cycle 1> [11] 0.00 0.00 34847/186997954 zerop [7] 0.00 0.00 1/1806 make_zero [16] 34846 even <cycle 1> [13]
第[10]段里的 1 + 69693 显示了循环 1 被调用了一次,而循环里的函数被调用了 69,693 次。这个循环调用了 even 函数。下一项显示了 odd 函数被 even 函数调用了 34,847 次。
在本小节中,我们简要的讨论了 gprof 的一些特性。它的 info 页包含了其它一些很有用的功能说明:
- 使用 -s 选项来求几次不同运行的执行结果和。
- 使用 -c 选项来辨别子函数有没有被调用。
- 使用 -l 选项来逐行显示性能信息
- 使用 -A 选项来显示添加了执行代码百分比的经过注释的源代码。
这个信息页同时也包括了更多关于如何分析展示数据的信息。
gprof 如何收集数据
当一个可执行程序运行时,每次调用函数时对应的计数器就会被加 1。同时,gprof 会周期性中断进程以判断当前正在执行的函数。这些取样数据确定了函数的执行次数(原文:execution times)。因为 Linux 的时钟周期为 0.01 秒,这些中断最多每 0.01 秒发生一次。因此对执行迅速的程序或者执行迅速而调用频率很低的函数的统计结果可能不精确。为了避免这种错误,应该尝试延长程序执行时间,或者把几次执行的所有数据进行汇总。阅读 gprof 的 info 页中关于利用 -s 选项进行多次数据汇总的说明。
计算器程序源代码
代码 A.3 显示了一个计算后缀表达式的程序。
代码A.3 (calculator.c) 计算器主程序
/* 使用一元数计算。 */ /* 使用后缀标记法输入一行表达式,比如, 602 7 5 - 3 * + 非负数使用十进制输入法。支持 “+”,“-” 和“*”操作。一元操作符 “偶数”和“奇数”在一个操作数是偶数或奇数时返回数字1。 所有字都必须用空格隔开。不支持负数。*/ #include <stdio.h> #include <stdlib.h> #include <string.h> #include <ctype.h> #include “definitions.h” /* 提供栈中获得操作数的二元操作,把结果压栈。成功返回非零数。*/ int apply_binary_function (number (*function) (number, number), Stack* stack) { number operand1, operand2; if (empty_stack (*stack)) return 0; operand2 = pop_stack (stack); if (empty_stack (*stack)) return 0; operand1 = pop_stack (stack); push_stack (stack, (*function) (operand1, operand2)); destroy_number (operand1); destroy_number (operand2); return 1; } /* 提供栈中获得操作数的一元操作,把结果压栈。成功返回非零数。 */ int apply_unary_function (number (*function) (number), Stack* stack) { number operand; if (empty_stack (*stack)) return 0; operand = pop_stack (stack); push_stack (stack, (*function) (operand)); destroy_number (operand); return 1; } int main () { char command_line[1000]; char* command_to_parse; char* token; Stack number_stack = create_stack (); while (1) { printf (“Please enter a postfix expression:/n”); command_to_parse = fgets (command_line, sizeof (command_line), stdin); if (command_to_parse == NULL) return 0; token = strtok (command_to_parse, “ /t/n”); command_to_parse = 0; while (token != 0) { if (isdigit (token[0])) push_stack (&number_stack, string_to_number (token)); else if (((strcmp (token, “+”) == 0) && !apply_binary_function (&add, &number_stack)) || ((strcmp (token, “-”) == 0) && !apply_binary_function (&subtract, &number_stack)) || ((strcmp (token, “*”) == 0) && !apply_binary_function (&product, &number_stack)) || ((strcmp (token, “even”) == 0) && !apply_unary_function (&even, &number_stack)) || ((strcmp (token, “odd”) == 0) && !apply_unary_function (&odd, &number_stack))) return 1; token = strtok (command_to_parse, “ /t/n”); } if (empty_stack (number_stack)) return 1; else { number answer = pop_stack (&number_stack); printf (“%u/n”, number_to_unsigned_int (answer)); destroy_number (answer); clear_stack (&number_stack); } } return 0; }
代码 A.4 利用空链表实现的一元数。
代码 A.4 (number.c) 一元数实现
/* 一元数操作 */ #include <assert.h> #include <stdlib.h> #include <limits.h> #include “definitions.h” /* 创建代表0的数。 */ number make_zero () { return 0; } /* 如果这个数字代表0则返回非零。 */ int zerop (number n) { return n == 0; } /* 正数减1。 */ number decrement_number (number n) { number answer; assert (!zerop (n)); answer = n->one_less_; free (n); return answer; } /* 数加1。*/ number add_one (number n) { number answer = malloc (sizeof (struct LinkedListNumber)); answer->one_less_ = n; return answer; } /* 销毁一个数。 */ void destroy_number (number n) { while (!zerop (n)) n = decrement_number (n); } /* 复制一个数。这个函数只为内存分配而存在。 */ number copy_number (number n) { number answer = make_zero (); while (!zerop (n)) { answer = add_one (answer); n = n->one_less_; } return answer; } /* 两个数相加。 */ number add (number n1, number n2) { number answer = copy_number (n2); number addend = n1; while (!zerop (addend)) { answer = add_one (answer); addend = addend->one_less_; } return answer; } /* 从一个数减去另一个数。 */ number subtract (number n1, number n2) { number answer = copy_number (n1); number subtrahend = n2; while (!zerop (subtrahend)) { assert (!zerop (answer)); answer = decrement_number (answer); subtrahend = subtrahend->one_less_; } return answer; } /* 返回两数操作结果。 */ number product (number n1, number n2) { number answer = make_zero (); number multiplicand = n1; while (!zerop (multiplicand)) { number answer2 = add (answer, n2); destroy_number (answer); answer = answer2; multiplicand = multiplicand->one_less_; } return answer; } /* 偶数则返回非零。 */ number even (number n) { if (zerop (n)) return add_one (make_zero ()); else return odd (n->one_less_); } /* 奇数返回非零。 */ number odd (number n) { if (zerop (n)) return make_zero (); else return even (n->one_less_); } /* 转化一个代表整数数字的字符串为“数”。 */ number string_to_number (char * char_number) { number answer = make_zero (); int num = strtoul (char_number, (char **) 0, 0); while (num != 0) { answer = add_one (answer); --num; } return answer; } /* 转化一个 “数” 为一个 “unsigned int”. */ unsigned number_to_unsigned_int (number n) { unsigned answer = 0; while (!zerop (n)) { n = n->one_less_; ++answer; } return answer; }
代码 A.5 的函数利用链表实现了一个一元数的栈代码 A.5 (stack.c) 一元数栈
/* 提供容纳“数”的栈。*/ #include <assert.h> #include <stdlib.h> #include “definitions.h” /* Create an empty stack. */ Stack create_stack () { return 0; } /* 栈空则返回非零值。 */ int empty_stack (Stack stack) { return stack == 0; } /* 移除非空栈栈顶的数。空栈则退出。 */ number pop_stack (Stack* stack) { number answer; Stack rest_of_stack; assert (!empty_stack (*stack)); answer = (*stack)->element_; rest_of_stack = (*stack)->next_; free (*stack); *stack = rest_of_stack; return answer; } /* 在栈开始的位置加一个数。*/ void push_stack (Stack* stack, number n) { Stack new_stack = malloc (sizeof (struct StackElement)); new_stack->element_ = n; new_stack->next_ = *stack; *stack = new_stack; } /* 移除所有的栈元素。 */ void clear_stack (Stack* stack) { while (!empty_stack (*stack)) { number top = pop_stack (stack); destroy_number (top); } }
代码 A.6 包含了栈和数字的声明。代码 A.6 (definitions.h) number.c 和 stack.c 的头文件
#ifndef DEFINITIONS_H #define DEFINITIONS_H 1 /* 用链表实现一个数。 */ struct LinkedListNumber { struct LinkedListNumber* one_less_; }; typedef struct LinkedListNumber* number; /* 用链表实现栈数。使用0表示一个空栈。 */ struct StackElement { number element_; struct StackElement* next_; }; typedef struct StackElement* Stack; /* 针对栈的操作 */ Stack create_stack (); int empty_stack (Stack stack); number pop_stack (Stack* stack); void push_stack (Stack* stack, number n); void clear_stack (Stack* stack); /* 针对数的操作 */ number make_zero (); void destroy_number (number n); number add (number n1, number n2); number subtract (number n1, number n2); number product (number n1, number n2); number even (number n); number odd (number n); number string_to_number (char* char_number); unsigned number_to_unsigned_int (number n); #endif /* DEFINITIONS_H */
1 在一个后缀表达式中,操作数间的二进制操作符被放置在两个操作数之后。举例而言,6 * 8 将被表示为 6 8 *;而 6 * 8 + 5 则应被表示为 6 8 * 5 +。
原文地址:http://sourceforge.net/apps/trac/elpi/wiki/AlpProfiling