Linux 下批量计时中的问题

Linux 下批量计时中的问题
内容:
使用 C 语言对函数批量计时
使用C++对函数批量计时
Shell下的批量计时
结束语
参考资料
关于作者
对本文的评价
订阅:
developerWorks 时事通讯

车皓阳
中科院软件所
2004 年 4 月

在作者的 前面一篇文章中,讨论了在Linux下与计时相关的一些基本问题,以及解决这些问题的手段和方法。本文将会关注大规模测试下的计时方法问题,以及中间会遇到的一些异常情况及其应对的策略。

这里所谓的大规模,意思是指成百上千的函数或可执行文件,也就是说,不是针对某一个函数或文件来进行计时操作。大致上,可以划分为两种情形:1、使用程序设计语言如,C 或 C++ 在文件内部对函数进行批量计时;2、在 Shell 下使用脚本语言批量对文件执行时间进行计时。在计时的过程中,还会遇到系统时钟改变等一些的特殊情况,这时我们要如何处理?

因为 Linux 下的开发语言很多,每一种语言都有自己的使用风格和特色,我们不可能针对每一种语言都来探讨其中的批量计时问题,这里只选用具有典型特色的 C、C++ 和 Shell Script 语言来进行描述。

使用 C 语言对函数批量计时
问题的典型场景:某程序员因工作需要编写了数量众多的C函数,之后有要求记录所有这些函数的运行时间,以完成性能评测。

为了讨论的方便,我们先假定待测函数都是无参数返回类型相同的函数。在实际操作过程中,经常遇到的也就是这一种情况。对于计时来说,函数返回类型是什么并不重要,因为我们只是测试函数调用执行时间,而并不关心返回结果。传入参数对函数调用是有影响的,而且也很可能会影响到函数运行的时间。但为了简化问题,我们还是把讨论的范围限制在无参数的情形下。如果出现极端的情形,每一个待测函数都有着不同的参数列表,那么只能针对每个函数都测量一次结果,而别无它法。下面是我们的想法。

最原始的解决办法是针对每一个函数都前后两次调用 C 函数 gettimeofday,然后做差值。以前经常也会用函数 time 来取时间,但现在它已经被 gettimeofday 替代了。

清单1. 使用 gettimeofday 在 C 语言里进行计时


gettimeofday( &start, NULL );
foobar();
gettimeofday( &end, NULL );
timeuse = 1000000 * ( end.tv_sec - start.tv_sec ) + end.tv_usec - start.tv_usec;
timeuse /= 1000000;

最后将得到的时间值输出到控制台或文件当中。由于待测的函数总是会先编写出来,这样做,最大的一个问题就是针对每个函数都要重复拷贝上面一段代码,结果是代码冗余度太高,代码质量也随之下降,并且还不具通用性。程序员的心理都是要创造出高效、实用且美观的代码,象这种质量的代码程序员是无法接受的。

现在问题已经很清楚了,那就是如何使用尽可能少的语句来完成函数的批量计时任务,同时要保证代码具有较好的通用性。我们的想法如下。为了消除计时和输出代码的冗余,必定要用到循环,在循环体内对每一个函数执行计时操作,这样可以使冗余代码量大大降低。但调用函数的语句是肯定要存在,所以现在的目标就是如何在循环体内依次调用程序中的出现的函数,为了设定循环次数,又需要弄清楚程序中到底有多少个函数。

ANSI C中并没有如同类似现在JAVA等面向对象语言中反射或自省的功能,不可能做到在运行时动态调用方法,也无法动态得知一些运行时信息。因此在文件中编写动态调用方法的代码这条路线肯定是不通的,那么就必须预先得到所有的待测函数名称,也就是说先要静态指定函数名称。

我们采用函数指针,函数指针可以代替switch/if语句,实现晚绑定和回调功能。在这里,我们仅用函数指针来实现同类型函数的调用。

清单2. 使用函数指针实现同类型函数的调用


#define numFuncs 2

int foobar1() { …… }
int foobar2() { …… }
……

typedef int ( *ptFuncDef )();
ptFuncDef ptFuncArr[] = { &foobar1, &foobar2, …… };
……

for ( i = 0; i < numFuncs; i++ ) {
	gettimeofday( &start, NULL );
	ptFuncArr[i]();
	gettimeofday( &end, NULL );
	……
}

函数指针确实可以使代码数量大大降低,但现在的问题是数组ptFuncArr初始化列表要如何生成。几百个函数,如果全部由手工在后面添加的话,会是一件很烦琐的工作。有没有可以提取源文件中所有函数名称的工具?

一些可视化的编辑工具,如Source Insight、Source Navigator等都可以识别C语言的函数,可以将所有用到的函数名称,除main外,拷贝生成文本文件。

清单3. 文本文件的内容


foobar1
foobar2
……

要生成初始化列表,可以使用sed命令对文本文件的内容进行替换,加上前缀"&"和后缀","。

清单4. 使用sed命令把in.file中每一行都加上前缀"&"和后缀","


sed -e 's/^//&/' -e 's/$/,/' in.file > out.file

清单5. out.file文件的内容


&foobar1,
&foobar2,
……

如果想要五个函数一行,可以执行下面的Shell Script代码。

清单6. 将out.file文件每五行内容转换为一行的Shell脚本


#!bin/bash
i=0
while read line
do
i=`expr $i + 1`
if [ $i -lt 5 ] ; then
   echo $line | tr -d "/n" >> "new.file"
else
   echo $line >> "new.file"
   i=0
fi
done < "out.file"

记着要把new.file文件中最后一个","删除,然后用新文件的内容填充数组ptFuncArr初始化列表。

在批量计时的过程中,一些问题会凸显出来。例如,中途系统时钟被改变了,会怎么样?C函数gettimeofday将会依据新的时间来计时,计时结果将会发生错误。这时可以使用类似Windows中GetTickCount的C函数times或clock。

清单7. clock函数的用法


#include<time.h>
……
begin = clock();
……
end = clock();
fprintf( fh, "%8.2g/n", (begin - end) / CLOCKS_PER_SEC );

这样就会避免发生上述问题,排除系统时钟改变的干扰。

并且,有人曾经做过测试,连续两次使用gettimeofday时,会以一种小概率出现"时光倒流"的现象,第二次函数调用得到的时间要小于或说早于第一次调用得到的时间。gettimeofday函数并不是那么稳定,没有times或clock计时准确,但它们用法相似。clock有计时限制,据说是596.5+小时,一般情况足以应付。

使用C++对函数批量计时
C++兼容C,我们同样可以使用上面的方法来解决问题。C++是面向对象的语言,因此,最好把计时功能封装成类来使用,可以参考boost库中定义的timer类。

清单8. timer类


class timer
{
public:
timer() { _start_time = std::clock(); } 
void   restart() { _start_time = std::clock(); } 
double elapsed() const
    { return  double(std::clock() - _start_time) / CLOCKS_PER_SEC; }
		
double elapsed_max() const
{
    return (double(std::numeric_limits<std::clock_t>::max())
       - double(_start_time)) / double(CLOCKS_PER_SEC); 
}
		
double elapsed_min() const
   	{ return double(1)/double(CLOCKS_PER_SEC); }
		
private:
std::clock_t _start_time;
}; // timer

需要注意的是,在C++文件中定义的函数指针指向的是用户自定义类的成员函数。除此之外,其余地方使用方法大致相同。

清单9. 指向成员函数的指针


int ( CMyClass::*ptMemberFunc )() = &CMyClass::MemberFunc;
CMyClass instance;
(instance.*ptMemberFunc)();

还有一点就是,在C++里可以使用vector存储函数指针,不再使用静态数组,vector的作用相当于动态数组,支持对内存的re-allocation。

清单10. 使用vector代替数组


	#include <vector>
	using namespace std;
	……
	typedef int ( *ptFuncDef )();
	vector< ptFuncDef > ptFuncVec;
	timer ttimer;
	
	ptFuncVec.push_back( &CMyClass::foobar1 );
	ptFuncVec.push_back( &CMyClass::foobar2 );
	
	for (int i = 0; i < ptFuncVec.size(); i++) {
		ttimer.restart();
		ptFuncVec[i]()
		fprintf( fh, "%8.2g/n", ttimer.elasped() );
		......
	}

上面这段代码里,需要多次使用push_back将类成员函数地址置入vector,这是很麻烦的。虽然如此,但其它如设计模式中的一些方法, 或许还包括VPTR,都过于复杂了,对于这样一个问题似乎有些大材小用,因此建议还是以此出发点来考虑这个问题。

Shell下的批量计时

典型场景1:在某个目录下有一个可执行程序,它接受不同的输入参数。现在有要求在输入几百个不同参数的情况下,测试执行时间,并将结果写入文件中,每一个结果占用一行,以方便后续处理。

首先假设测试参数存储在一个输入文件"in.file"当中。

清单11. 文件in.file中的内容


1.00 2.00
1.50 2.76
1.80 3.30
……

现在的目标就是每次一行地读出文件中的参数,送给待测可执行程序,作为其执行参数,具体的代码段如下。

清单12. 测量不同参数的同一待测可执行程序在Linux下的运行时间


	#!bin/bash
while read line
do
    i=`echo $line | cut -f1 -d" "`
    j=`echo $line | cut -f2 -d" "`
    (time foobar $i $j) 2>foo.tmp
    grep real foo.tmp | cut -f2 >>foo.txt
done < "in.file"
rm -f foo.tmp

这里用的是Linux内置的time命令,当然也可以使用GNU的time命令(/usr/bin/time,精度要低一些)。cut命令里"-d"选项指定分隔符,分隔符默认是TAB。在待测可执行程序拥有多个参数的情况下,要使用多个变量来记录,cut命令里"-f"选项后面跟随的数字指定不同的域。再详细的说明可以参见作者的 前面一篇文章

典型场景2:在某个目录下有好几百个可执行程序,要测试每一个可执行程序的运行时间,并将结果写入文件中,每一个可执行程序都要在文件中占用一行。

假定这些待测可执行程序都不需要指定输入参数,同时假定目录/home/developerworks/test/下的所有文件均为待测可执行文件。

清单13. 测量同一目录下所有的可执行程序在Linux下的运行时间


	for file in /home/developerworks/test/*
	do
	(time $file) 2>foo.tmp
    	grep real foo.tmp | cut -f2 >>foo.txt
	done

time命令结果有三行组成:real、user和sys。我们这里用的都是real值,它表示从程序开始到程序执行结束时所消耗的时间,包括CPU的用时。CPU用时被划分为user和sys两块。user值表示程序本身,以及它所调用的库中的子例程使用的时间。sys是由程序直接或间接调用的系统调用执行的时间。

在单处理器上,real值和整个CPU用时之差,也就是real - ( user + sys )是所有延迟程序执行的因素的总和。在SMP上,这个值近似为real * number_of_processors - ( user + sys )。这些因素包括:

  • 调入程序文本和数据的IO操作

  • 获取程序实际使用内存的IO操作

  • 由其它程序消耗的CPU用时

  • 由操作系统消耗的CPU用时

  • Shell下处理计时相对来说很简单,它是以牺牲计时精确性为代价的。相比之下,在语言中进行处理就显得要复杂得多,但精度更高。

结束语
必须要承认,上面的内容全部只是考虑理想状态下的情形,我们限制了函数参数,返回值的类型,而且也假定不存在多线程并发现象。但有了这些内容作为基础,再深入研究复杂的场景,就会变得容易一些。

参考资料

关于作者
车皓阳是中科院软件所的一名博士生,目前关注实时调度算法相关的内容。您可以通过 grandiose11@msn.com与其联系,欢迎讨论相关问题。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值