如何精确测量程序运行时间

    技术2022-05-11  2

     

    前言

    Contents

    1 前言 2 进程调度和模式切换 3 方法一:间隔计数 4 方法二:周期计数 5 方法三:gettimeofday函数计时 6 总结

    对于一个嵌入式程序员来说,“我的程序到底运行多快”,是我们最为关心的问题,因为速度,实时性,永远是嵌入式设备性能优化的基本立足点之一。 可惜的是,我们平时常用的测试运行时间的方法,并不是那么精确的。换句话说,想精确获取程序运行时间,不是那么容易的。也许你会想,程序不就是一条条指令 么,每一条指令序列都有固定执行时间,为什么不好算?真实情况下,我们的计算机并不是只运行一个程序的,进程的切换,各种中断,共享的多用户,网络流量, 高速缓存的访问,转移预测等,都会对计时产生影响。

    可惜的是,在性能测量领域,我们有gprof,有intel的vtune,却缺少相应 的,广泛流传的参考文献。如果你希望能建立起自己的工具,或者对具体的测量方式感兴趣,那么本文也许会对你有帮助。我想,应该有很多人希望知道计时机制的 原理,因为针对不同的系统,环境,会有不同的解决方案。本文主要针对Linux和X86体系环境,主要思想来源于”Computer System A Programmer’s Perspective“,夹杂了一些自己的理解,并试图给出我自己写的一个通用测量工具,支持用户自配置。本文有时的对象是程序有时描述对象是进程,这个请自行理解,因为一个程序就是在一个进程里面执行的。

     

    进程调度和模式切换

    在介绍具体方法之前,先简单说几句。

    对 于进程调度来讲,花费的时间分为两部分,第一是计时器中断处理的时间,也就是当且仅当这个时间间隔的时候,操作系统会选择,是继续当前进程的执行,还是切 换到另外一个进程中去。第二是进程切换时间,当系统要从进程A切换到进程B时,它必须先进入内核模式将进程A的状态保存,然后恢复进程B的状态。因此,这个切换过程是有内核活动来消耗时间的。具体到进程的执行时间,这个时间也包括内核模式和用户模式两部分,模式之间的切换也是需要消耗时间,不过都算在进程执行时间中了。

    其实模式切换非常费时,这也是很多程序中都要采用缓冲区的原因,例如,如果每读一小段文件什么的就要调用一次 read之类的内核函数,那太受影响了。所以,为了尽量减少系统调用,或者说,减少模式切换的次数,我们向程序(特别是IO程序)中引入缓冲区概念,来缓 解这个问题。

    一般来说呢,向处理器发送中断信号的计时器间隔通常是1-10ms,太短,切换太多,性能可能会变差,太长呢,如果在任务间切换频繁,又无法提供在同时执行多任务的假象。这个时间段,也决定了一些我们下面要分析的不同方法衡量时间的差异。

    方法一:间隔计数

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

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

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

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

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

    直接调用time命令(一堆鸡蛋) 使用tms结构体和times函数

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

    clock_t times( struct tms *buf )

    这个tms的结构体为

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

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

    方法二:周期计数

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

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

    void counter( unsigned *hi, unsigned *lo ){asm(”rdtsc; movl %

    转载请注明原文地址: https://ibbs.8miu.com/read-900338.html

    最新回复(0)