程序的“听诊器”——性能监视工具( 三 )


性能监视工具对于性能之外的任务也有用 。 在找素数的练习中 , 它指出了程序P4的一个错误 。 行计数在估计测试覆盖面时极有价值 , 比如 , 如果出现零 , 则说明有代码未测试 。 DEC公司的Dick Sites这样描述性能监视的其他用途:“(1) 在两层微存储实现中 , 决定哪些微代码放到芯片上;(2) 贝尔北方研究院(Bell Northern Research)的一位朋友某个周末在带有多重异步任务的实时电话交换软件系统上实现了语句计数 。 通过查看异常计数 , 他发现了现场安装的代码中存在6处错误 , 所有错误都涉及不同任务之间的交互 。 其中一处错误用常规调试技术无法成功追踪到 , 其余错误还没有被当作问题(也就是说 , 这些错误症状可能已经发生 , 但是没有人能够将其归结为具体的软件错误) 。 ”
3 专用的性能监视工具到目前为止我们所看到的性能监视工具的原理 , 适用于从汇编和Fortran直到Ada这样的程序设计语言 , 但是很多程序员现在使用更强大的语言 。 如何监视Lisp或APL程序的计算性能?又如何监视网络或数据库语言程序的计算性能?
我们打算用UNIX的管道(pipeline)作为更有趣的计算模型的例子 。 管道是一系列的过滤程序(filter):当数据流经每个过滤程序时 , 对数据施加变换 。 下面这个经典的管道按照频率递减顺序打印某文件中使用最多的25个单词 。
cat $* |tr -cs A-Za-z '\012' |tr A-Z a-z |sort |uniq-c |sort -r –n |sed 25q当用这个管道在一本大约6万字的书中寻找25个最常见的单词时 , 我们监视这个管道的性能 。 输出的前6行是:
3463 the1855 a1556 of1374 to1166 in1104 and...下面是对VAX-11/750上计算的“管道性能监视”:
lineswordscharstimes107175970134223314.4u 2.3s18rtr -cs A-Za-z \012576525765130489411.9u 2.2s15rtr A-Z a-z5765257651304894104.9u 7.5s 123rsort576525765130489424.5u 1.6s 27runiq –c 473194616183027.0u 1.6s 31rsort -rn 47319461618300.0u 0.2s 0rsed 25q2550209左边几列说明每个阶段的数据:行数、单词数、字符数 。 右边部分描述了数据阶段之间的过滤程序:用秒表示的用户时间、系统时间以及真实时间 , 后面是命令本身 。
这个性能监视结果给出了程序员感兴趣的许多信息 。 这个管道是快速的 , 处理150页的书只需3.5分钟 。 第一次排序花了这个管道57%的运行时间 , 这种经过仔细调优的实用程序很难再提速了 。 第二次排序只花了这个管道14%的时间 , 但是还有调优的余地 。 这个性能监视结果还发现了管道中隐藏的一处小错误 。 UNIX高手们会乐于找出引入空行的地方 。
这个性能监视结果也透露了文件中单词的信息:共有57 651个单词 , 但只有4731个不同的单词 。 在第一个翻译程序之后 , 每个单词有4.3个字母 。 输出表明 , 最常见的单词是“the” , 占了文件的6% 。 6个最常见的单词占了文件的18% 。 对英语中最常见的100个单词做专门处理也许还能提高速度 。 试试看从这些计数中找出其他有趣的表面规律 。
跟许多UNIX用户一样 , 我过去也用手工监视管道的性能 , 利用单词计数(wc)命令来统计文件 , 用time命令来统计进程 。 “管道性能监视工具”让这个任务自动化了 。 用管道和一些输入文件的名称作为输入 , 产生性能监视结果作为输出 。 2个小时和50行代码就足以建立这个性能监视工具 。 下一节详细阐述这个话题 。
4 开发性能监视工具开发一个真正的性能监视工具是件困难的事情 。 Peter Weinberger开发了C行计数性能监视工具 , 我们前面看到的输出就是这个工具产生的 。 他在几个月时间内断断续续干了好几周才完成这个项目 。 本节描述如何更容易地开发一个简化版本 。
Dick Sites声称他的朋友“在某个周末实现了语句计数” 。 我觉得这简直难以置信 , 于是我决定要试着为附录A描述的Awk语言(这种语言还没有性能监视工具)开发一个性能监视工具 。 几小时后 , 当我运行程序P6的Awk版本时 , 我的性能监视工具生成了如下输出 。
程序P6及性能监视工具生成的输出
BEGIN { <<<1>>>n = 1000x[0] = 2; xc = 1print 2for (i = 3; i <= n; i++) { <<<998>>>if (prime(i)){ <<<167>>>print i}}exit}function prime(n,i) { <<<998>>>for (i=0; x[i]*x[i]<=n; i++) { <<<2801>>>if (n % x[i] == 0) { <<<831>>>return 0}}{ <<<167>>> }x[xc++] = nreturn 1}在左花括号后尖括号内的数显示该语句块被执行了多少次 。 幸运的是 , 这些计数与C行计数器产生的计数一样 。
我的性能监视工具包含两个5行的Awk程序 。 第一个程序读Awk源程序并且写一个新程序 , 其中在每个语句块开始的地方给不同的计数器加1;而在执行结束时 , 一个新的END动作(见附录A)把所有计数写入一个文件 。 当这样得出的程序运行时 , 就生成一个计数文件 。 第二个程序读出这些计数 , 把这些计数合并到源文本中 。 带性能监视的程序大约比原来的程序慢25% , 而且并不是所有的Awk程序都能正确处理——为了监视几个程序的性能 , 我不得不做出整行(one-line)的修改 。 但对于所有这些缺点来说 , 搭起一个能运行的性能监视工具 , 花几小时并不算什么大投入 。