Linux下高精度的时间测量

来源:互联网 发布:专科和本科的区别知乎 编辑:程序博客网 时间:2024/04/30 23:48

转自 http://hi.baidu.com/ybzzzzz/blog/item/2bf442181f6e8db24bedbc6d.html

想在Linux下实现高精度的时间测量,需要用到gettimeofday这个函数,它提供了微妙级的时间精度,函数定义如下:




gettimeofday
(取得目前的时间)

相关函数

timectimeftimesettimeofday

表头文件

#include <sys/time.h>
#include <unistd.h>

定义函数

int gettimeofday ( struct timeval * tv , struct timezone * tz )

函数说明

gettimeofday()会把目前的时间有tv所指的结构返回,当地时区的信息则放到tz所指的结构中。
timeval
结构定义为:
struct timeval{
long tv_sec; /*
*/
long tv_usec; /*
微秒*/
};
timezone
结构定义为:
struct timezone{
int tz_minuteswest; /*
Greenwich 时间差了多少分钟*/
int tz_dsttime; /*
日光节约时间的状态*/
};
上述两个结构都定义在/usr/include/sys/time.htz_dsttime 所代表的状态如下
DST_NONE /*
不使用*/
DST_USA /*
美国*/
DST_AUST /*
澳洲*/
DST_WET /*
西欧*/
DST_MET /*
中欧*/
DST_EET /*
东欧*/
DST_CAN /*
加拿大*/
DST_GB /*
大不列颠*/
DST_RUM /*
罗马尼亚*/
DST_TUR /*
土耳其*/
DST_AUSTALT /*
澳洲(1986年以后)*/

返回值

成功则返回0,失败返回-1,错误代码存于errno。附加说明EFAULT指针tvtz所指的内存空间超出存取权限。

范例

#include<sys/time.h>
#include<unistd.h>
main(){
struct timeval tv;
struct timezone tz;
gettimeofday (&tv , &tz);
printf(
tv_sec; %d/n, tv,.tv_sec) ;
printf(
tv_usec; %d/n,tv.tv_usec);
printf(
tz_minuteswest; %d/n, tz.tz_minuteswest);
printf(
tz_dsttime, %d/n,tz.tz_dsttime);
}

执行

tv_sec: 974857339
tv_usec:136996
tz_minuteswest:-540
tz_dsttime:0

 

gettimeofday的实现机制在不同系统上是不一样的,而且虽然变量名叫usec,其实没这么精确。具体的精确程度,是和系统相关的。比如在Linux下,是用周期计数来实现的,所以和周期计数的精确度差不多,但是在Windows NT下,是使用间隔计数实现的,精确度就很低了。

关于间隔计数和周期计数的实现机制摘抄如下:

一:间隔计数

我们都知道,Linux下有一个命令是专门提供一个进程的运行时间的,也就是timetime可以测量特定进程执行时所需消耗的时间及系统资源等,这个时间还可以分内核时间和用户态时间两部分呈现给你。它是怎么做到的呢?其实很简单,操作系统本身就是用计时器来记录每个进程使用的累计时间,原理很简单,计时器中断发生时,操作系统会在当前进程列表中寻找哪个进程是活动的,一旦发现,哟,进程A跑得正欢,立马就给进程A的计数值增加计时器的时间间隔(这也是引起较大误差的原因,想想)。当然不是统一增加的,还要确定这个进程是在用户空间活动还是在内核空间活动,如果是用户模式,就增加用户时间,如果是内核模式,就增加系统时间。

原理很简单吧?但是相信一点,越简单的东西,是不会越精确的,人品守恒,能量守恒,难度也当然会守恒了啊。下面就简单分析一下,为啥这玩意精度不高吧。举个例子,如果我们有一个系统,计时器间隔为10ms,系统里面跑了一个进程,然后我们用这种方法分析时间,测出70ms,想一想,实际会有几种结果?具体点,我们用这种方法对进程计时,在某个计时器中断时,系统发现,咦,有一个进程开始跑了,好,给进程的计数值加上10ms。但是实际上呢,这个进程可能是一开始就跑起来了,也肯能是在中断的前1ms才开始跑的。不管是什么原因,总之中断时候它在跑,所以就得加10ms。当中断发生时发现进程切换了,同理,可能是上一个中断之后1ms进程就切换了,也可能人家刚刚才切换。

所以呢,如果一个进程的运行时间很短,短到和系统的计时器间隔一个数量级,用这种方法测出来的结果必然是不够准确的,头尾都有误差。不过如果程序的时间足够长,这种误差有时能够相互弥补,一些被高估一些被低估,平均下来刚好,呵呵。从理论上,我们很难分析这个误差的值,所以一般只有程序到达秒的数量级时,用这种方式测试程序时间才有意义。

说了半天,难道这方法没优点了?不,这个世界没有纯善,也没有纯恶。这方法最大的优点是,它的准确性不是非常依赖于系统负载。那什么方法依赖于系统负载呢?接下来我们会讲到:)

理论陈述结束,我想应该开始关注实现方法了吧。其实超级简单,两种方法:

1.    直接调用time命令(一堆鸡蛋)

2.    使用tms结构体和times函数

说说正经点的第二个方法吧。在Linux中,提供了一个times函数,原型是

clock_t times( struct tms *buf )

这个tms的结构体为

struct tms
{
    clock_t tms_utime;       // user time
    clock_t tms_stime;       // system time
    clock_t tms_cutime;     // user time of reaped children
    clock_t tms_cstime;     // system time of reaped children
}

怎么使用就不用这里教了吧?不过要说明一下的是,这里的cutimecstime,都是对已经终止并回收的时间的累计,也就是说,times不能监视任何正在进行中的子进程所使用的时间。

二:周期计数

刚才谈了半天间隔计数的不足之处,哪有不足,那就有弥补的方法,特别实在万能的Linux:) 为了给计时测量提供更高的准确度,很多处理器还包含一个运行在时钟周期级别的计时器,它是一个特殊的寄存器,每个时钟周期它都会自动加1。这个周期计数器呢,是一个64位无符号数,直观理解,就是如果你的处理器是1GHz的,那么需要570年,它才会从264次方绕回到0,所以你大可不必考虑万一溢出怎么办此类问题。

看到这里,也许你会想,哇塞,很好很强大嘛,时钟周期,这都精确到小数点后面多少位来着了?这下无论是多快的用时多短的程序,我们也都能进行时间测量了。Ohyeah。等等,刚才我们说过什么来着?守恒定律啊!功能强大的东西,其他方面必有限制嘛。看到上面的介绍,聪明的你一定能猜出来这种方法的限制是什么了,那就是,hardware dependent。首先,并不是每种处理器都有这样的寄存器的,其次,即使大多数都有,实现机制也不一样,因此,我们无法用统一的,与平台无关的接口来使用它们。怎么办?这下,就要祭出上古传说中的神器:汇编了。当然,我们在这里实际用的是C语言的嵌入汇编:

void counter( unsigned *hi, unsigned *lo )
{
asm("rdtsc; movl %%edx,%0; movl %%eax, %1"
        : "=r" (*hi), "=r" (*lo)
        :
        : "%edx", "%eax");
}

第一行的指令负责读取周期计数器,后面的指令表示将其转移到指定地点或寄存器。这样,我们将这段代码封装到函数中,就可以在需要测量的代码前后均加上这个函数即可。最后得到的hilo值都是两个,除了相减得到间隔值外,还要进行一些处理,在此先按下不表。

不得不提出的是,周期计数方式还有一个问题,就是我们得到了两次调用counter之间总的周期数,但我们不知道是哪个进程使用了这些周期,或者说处理器是在内核还是在用户模式中。还记得刚才我们讲间隔计数方式么?这玩意的好处就是它是操作系统控制给进程计时的,我们可以知道具体哪个进程,哪个模式。但是周期计数只测量经过的时间,他不管你是哪个进程使用的。所以,用周期计数的话,我们必须很小心。举个例子

double time()
{
     start_counter();
     p();
     get_counter();
}

这样一段程序,如果机器的负载很重,会导致P运行时间很长,而其实P函数本身是不需要运行这么长时间的,而是上下文切换等过程将它的时间拖长了。

而且,转移预测(想一想,如果转移方向和目的预测错误)和高速缓存的命中率,对这个计数值也会有影响。通常情况下,为了减少高速缓存不命中给我们程序执行时间带来的影响,可以执行这样的代码:

double time_warm( void )
{
     p();
     start_counter();
     p();
     get_counter();
}

原因不用我再解释了吧?它让指令高速缓存和数据高速缓存都得到了warm-up

好,接下来又有问题。如果我们的应用,是属于那种每次执行都希望访问新的数据的那种呢?在这种情况下,我们希望让指令高速缓存warm-up,而数据高速缓存不能warm-up,很明显,time_warm函数低估我们的运行时间了。让我们进行进一步修改:

double time_cold( void )
{
     p();
     clear_cache();
     start_counter();
     p();
     get_counter();
}

注意,我们加入了一个清除数据缓存的函数。这个函数的具体实现很简单,依情况而定,比如举个例子

volatile int tmp;
static int dummy[N];      // N
是你需要清理缓存的字节数

void clear_cache( void )
{
     inti, sum = 0;
     for( i=1;i<N;i++ )
          dummy[i] = 2;
     for( i=1;i<N;i++ )
          sum += dummy[i];
     tmp = sum;
}

具体原理很简单,我们在定义一个数组并在其上执行一个计算,计算过程中的数据会覆盖高速数据缓存中原有的数据。每一次的storeload都会让高速数据缓存cache这个数组,而定义为volatiletmp则保证这段代码不会被优化。

这样做,是不是就万无一失了呢?不是的,因为大多数处理器,L2高速缓存是不分指令和数据的,这样clear_cache会让所有P的指令也被清除,只不过:L1缓存中的指令还会保留而已。

其实上面提到的诸多原因,都是我们不能控制的,我们无法控制让高速缓存去加载什么,不去加载什么,加载时去掉什么,保留什么。而且,这些误差通常都是会过高估计真实的运行时间。那么具体使用时,有没有什么办法来改善这种情况呢?有,就是The K-Best Measurement Scheme。这玩意其实很麻烦,所以我在具体实践中都不用它,附上一个文档,有兴趣的朋友可以下载下来看一下。

原创粉丝点击