深入理解操作系统(23)第九章:测量程序执行时间(记录时间流逝的两种基本机制/时间尺度(微观和宏观)/计时器中断/间隔时间/time计时/gettimeofday)

1. 前沿

人们经常问的一个问题是:

程序x在机器Y上运行得有多快?

一个试图优化程序性能的程序员,或者一个想要决定买哪台机器的顾客,可能会提出这样的问题。在我们前面对性能优化的讨论中。我们试图把程序的CPE(每元素的周期数)测量值精确到小数点后两位。对一个CPE为10的过程,这要求精确度为0.1%。

在本章中,我们会讲述这个问题,并会发现它是非常复杂的。

你可能会以为在计算机系统上获得几近完美的计时测量会很简单。毕竟,对于某个程序和数据的组合,机器会执行固定的指令序列“指令的执行是由处理器时钟控制的,而处理器时钟是由精度振荡器控制的。不过,一个程序的执行与另一个程序的执行之间有许多因素是不同的。

计算机并不同时只执行一个程序 。
它们不停地从一个进程切换到另一个,为一个进程执行一些代码,然后再移到下一个进程。

对一个程序的处理器资源的准确调度依赖于这样一些因素,例如共享系统的用户数量、网络流量和对磁盘操作的计时。对高速缓存的访问模式不仅仅依赖于我们正在试图测量的程序的引用,还依赖于同时正在执行的其他进程,最后,分支预测逻辑会根据以往的历史猜测是否会选择分支。一个程序每次执行的历史都不相同。

1.1 记录时间流逝的两种基本机制

在本章中,我们描述计算机用来记录时间流逝的两种基本机制:

一种基于低频率计时器(timer),它会周期性中断处理器
另一种基于计数器(counter),每个时钟周期计数器会加1

应用程序的程序员可以通过调用库函数获得对前一种计时机制的访问。
有些系统上,可以通过库函数访问周期计时器(cycle timer),但是有些系统上需要编写汇编代码。我们将程序计时推迟到现在才讨论,是因为程序计时需要理解CPU硬件和操作系统管理进程执行的方式。

使用这两种计时机制,我们来研究获得程序性能的可靠测量值的方法。我们会看到,由上下文切换引起的计时变化会非常大,因此必须消除。由其他因素引起的变化,例如高速缓存和分支预测,通常是通过在精心控制的条件下执行程序操作来管理的。一般来说,我们可以获得对于非常短(小于大约10ms)或者非常长(大于大约1s)的时间段的准确测量值,即使是在负载很重的机器上。10ms-1s之间的时间要想准确测量需要特殊的处理。

1.2 没有专业的性能测量工具

许多对性能测量的理解都是计算机系统传说的一部分。不同的小组和个人开发了他们自己的测量程序性能的技术,但是关于这个主题没有广泛流传的文献。那些专业性能测量的公司和研究组,常常建立特殊配置的机器,使得造成计时不规则的来源最少,例如,通过限制访问或者关掉许多os和网络服务·我们希望能有程序员在普通机器上就能使用的方法,但是没有这样的广泛可获得的上具。所以,我们会开发我们自己的工具。

在这里的描述中,我们会系统地讲述这些问题。我们描述大量实验的设讦和评价,这些实验帮助我们获得在一规模系统上取得准确测量的方法。在一本这个层次的书中找到详细的实验研究还是不太常见的。通常,人们只想要最后的答案,而不想知道是怎样确定这些答案的,不过,在这里,对于如何在任意系统上测量任意程序的执行时间,我们不能提供确定的答案,有太多的计时机制、操作系统行为和运行时环境,不可能有一个惟一的、简单的解决方案。相反,我们期望你自己做实验,开发你自己的性能测量代码。我们希望我们的案例研究能帮助你完成这项任务。我们把我们的发现以协议的形式总结出来,它能够指导你的实验。

2. 计算机系统上的时间流

2.1 时间尺度

2.1.1 两种不同的时间尺度(微观+宏观)

计算机是在两个完全不同的时间尺度(timescale)上工作的。

1. 在微观级别,它们以每个时钟周期一条或多条指令的速度执行指令,
	这里每个时钟周期只需要大约1ns(纳秒)或者10-9s(1GHz为1ns)
2. 在宏观尺度上,处理器必须响应外部事件,
	外部事件发生的时间尺度要以ms(亳秒}或者10-3s来度量。

例如,在视频播放时,大多数计算机的图形显示器必须每33ms刷新一次。
保持世界记录的打字员敲键盘的速度也只能是大约每50ms一次击键。
磁盘通常需要大约10ms来启动一次磁盘传送。

2.1.2 解释:人感觉机器同时执行任务

在宏观时间尺度上,处理器不停地在许多任务之间切换。一次分配给每个任务大约5-20ms。

以这样的速度,用户感觉上任务是在同时进行的,因为人不能够察觉短于大约100ms的时间段。
在这段时间内,处理器可以执行几百万条指令。

2.1.3 计算机系统事件的时间尺度

图9.1在对数尺度上画出了各种事件类型的持续时间,

微观事件的持续时间以ns为单位(大)
宏观事件的持续时间以ms为单位(小)

图9.1
在这里插入图片描述

宏观事件是由OS例程来管理的,需要大约5000-200000个时钟周期。这些时间范围是以us来测量的(微秒)。
它比处理(大)宏观事件要快很多,以至于这些例程只给处理器增加了少量的负载。

2.2 进程调度和计时器中断

2.2.1 外部计时器(间隔时间 1-10ms)

外部事件,例如击键、磁盘操作和网络活动,会产生中断信号,这些中断信号使得操作系统调度程序得以运行,可能还会切换到另一个进程。即使没有这样的事件,我们也希望处理器从一个进程切换到另一个,这样用户看上去就好像处理器在同时执行许多程序一样。出于这个原因,

计算机有一个外部计时器,它周期性地向处理器发送中断信号。
这些中断信号之间的时间被称为间隔时间(intervaltime)

当计时器中断发生时,操作系统调度程序可以选择要么继续当前正在执行的进程,要么切换到另一个进程。这个间隔须设置得足够短,以保证处理器在任务间切换得足够频繁,能够提供在同时执行多个任务的假象,另一方面。从一个进程切换到另一个进程需要几千个时钟周期来保存当前进程的状态,并且为下一个进程准备好状态,因此将间隔设置得太短会导致性能很差。根据处理器以及处理器的配置情况,典型的计时器间隔范围是1-10ms。

2.2.2 例子说明(150ms的操作过程)

图9.2
在这里插入图片描述

在图中,计时器中断的发生是由短线标记来表示的

上图从系统的角度说明了在计时器间隔为lOms的系统上一个假设的150ms的操作。

在这段时间内有两个活动的进程:

A和B处理器交替地执行进程A的一部分,然后再执行B的一部分,依此类推。

当处理器执行这些进程时,它要么运行在用户模式,执行应用程序的指令,要么运行在内核模式,代表程序执行操作系统函数,例如处理缺页、输入或者输出。回想一下,内核操作被认为是每个普通进程的一部分,而不是一个独立的进程。每次有外部事件或者计时器中断时,都会调用操作系统调度程序。在图中,计时器中断的发生是由短线标记来表示的。这意味着在每个短线标记处都有一些内核活动,但是为了简便,在图中我们没有显不。

2.3 从应用程序的角度看时间(活动+不活动)

从应用程序的角度出发,可以把时间流看成两种时间段的交替,

一种时间段里程序是活动的(在执行它的指令)
另一种时间段里程序是不活动的(等待被操作系统调度)。

当应用的进程运行在用户模式中时,应用才能执行有用的计算。图9.2(b)说明了程序A是如何看待时间流的·在深灰色区域内应用是活动的,此时进程A正在用户模式中执行否则它是不活动的。

trace例子

3. 通过间隔计数来测量时间

操作系统也用计时器(timer)来记录每个进程使用的累计时间,这种信息提供的是对程序执行时间不那么准确的测量值。

3.1 操作

操作系统维护着每个进程使用的用户时间量和系统时间量的计数值,当计时器中断发生时,操作系统会确定哪个进程是活动的,并且对那个进程的一个计数值增加计时器间隔时间。如果系统是在内核模式中执行的,那么就增加系统时间,否则就增加用户时间。

图9.7(a)所示的例了表明了对两个进程的这种记账(accounting)。短线标记表明发生了计时器中断。每个计时器中断都由被增加的计数值来标识:或者是进程A的用户或系统时间Au或As,或者是进程B的用户或系统时Bu或Bs。每个短线标记是根据紧挨着它的左边的活动来标识的。最后的记账表明进程A总共使用了150ms:110ms的用户时间和40ms的系统时间;进程B总共使用了100ms,70ms的用户时间和30ms的系统时间。

3.2 读进程的计时器

3.2.1 time ./a.out 计算程序运行时间

当从Unix shell执行一个命令时,用户可以在命令前加上单词"time”,来测量命令的执行时间。
这个命令使用的值是用上面描述的记账方法计算出来的。
例子:

[root@localhost 8]# time ./a.out 
Hello sigaction
Hello sigaction
Hello sigaction
Hello sigaction
^Cfunc: I got signal 2
Hello sigaction
Hello sigaction
Hello sigaction
Hello sigaction
^C

real    0m7.468s
user    0m0.003s
sys     0m0.001s
[root@localhost 8]#

3.2.2 库函数times + struct tms + clock

程序员还可以通过调用库函数times来读进程的计时器,这个函数的声明如下:

times:

clock_t times(struct tms *buf);
函数功能 :获取进程时间。
times() 函数返回从过去一个任意的时间点所经过的时钟数。

struct tms:

struct tms 
{ 
	clock_t tms_utime ;          /* User CPU time.  用户程序 CPU 时间*/ 
	clock_t tms_stime ;          /* System CPU time. 系统调用所耗费的 CPU 时间 */ 
	clock_t tms_cutime ;         /* User CPU time of dead children. 已死掉子进程的 CPU 时间*/ 
	clock_t tms_cstime ;         /* System CPU time of dead children.  已死掉子进程所耗费的系统调用 CPU 时间*/ 
};//注: 这些时间都是时钟滴答数,而不是秒数。

clock:

ANSI C标准还定义了一个clock函数,它测量当前进程使用的总时间:

虽然它的返回值和函数使用的一样,都声明为clock_t类型但是通常这两个函数表达时间的单位是不一样的。
要将clock函数报告的时间变成秒数。

3.3 进程计时器的准确性

这些实验表明进程计时器只对获得程序性能的近似值有用。它们的粒度太粗,不能用于持续时间小于100ms的测量。
在这台机器上,这些进程计时器有系统偏差,过高地估计计算时间,平均大约4%。这种计时机制的主要优点是它的准确性不是非常依赖于系统负载。

4. 周期计数器

为了给计时测量提供更高的精确度,许多处理器还包含一个运行在时钟周期级的计时器。这个计时器是一个特殊的寄有器,每个时钟周期它都会加1。可以用特殊的机器指令来读这个计数器的值。不是所有的处理器都有这样的计数器的,而且有这样计数器的处理器在实现细节上也各不相同。因此,程序员无法用统一的、与平台无关的接口使用这些计数器。另一方面,只用少量的汇编代码,通常很容易就为某个特定的机器创建一个程序接口。

4.1 IA32周期计数器

到目前为止,我们己经报告的所有计时都是用IA32周期计数器(cycle counter)测量出来的。

在IA32体系结构中,周期计数器是与“P6”微体系结构(PentiumPro及其后续产品)一起提出来的。周期计数器是一个64位无符号数。对于一个运行时钟为1GHz的处理器,只有在每570年,这个计数器才会从2的64次方-1绕回到0。另一方面,如果我们只考虑这个计数器的低32位,把它看成一个32无符号整数,那么这个值会大约每4.3秒就绕回来。

因此,我们就明白了为什么IA32的设计者会决定实现一个64位的计数器。

IA32计数器是用rdtsc(read times tamp counter)读时间戳计数器指令来访问的。
这条指令没有参数。它将寄存器‰edx设置为计数器的高32位,而寄存器%e设置为低32位。为了提供一个c程序接口,我们想把这个指令包装到一个过程中。

5. 用周期计数器来测量程序执行时间

周期计数器(cycle counter)提供了一个非常精确的工具,可以测量一个程序执行中两个不同点之间经过的时间。
不过,典型地,我们对测量执行某段特殊代码所需要的时间感兴趣。

我们的周期计数器例程计算
	调用start-counter和调用get-counter之间总的周期数。

这些例程不记录哪个进程使用这些周期,或者处理器是在内核还是在用户模式中运行的:在使用这样的测量设备、来确定执行时间。

5.1 上下文切换的影响

5.2 高速缓存和其他因素的影响

5.3 k次最优测量方法

6. 基于gettimeofday 函数的测量

6.1 gettimeofday

另一个可能性是使用库函数gettimeofday()这个函数查询系統时钟(system clock)以确定当前的日期和时间。

#include"time.h"
struct timeval{
	long tv_sec;//秒
	long tv_usec;//微妙
}
int gettimeofday(struct timeval*tv, NULL);
	返回:若成功则为0,失败-1

这个函数把时间写入到一个调用者传递过来的结构中,这个结构包括一个单位为s的字段,还有一个单位为us的字段。第一个字段存放的是自从1970年1月1日以来经过的总秒数(对于所有的Unix系统来说,这都是一个标准的参考点注意,在Linux系统上,getumeofday的第二个参数,应该简单地置为NULL,因为它指向一个未被实现的执行时区校正的特性。

6.2 gettimeofday 例子说明

struct timeval us;
gettimeofday(&us,NULL);
//printf("gettimeofday: tv_sec=%ld, tv_usec=%ld\n", us.tv_sec, us.tv_usec);

参考:
linux 时间 time(1)-时间相关结构体和函数详解
https://blog.csdn.net/lqy971966/article/details/107975594

7. 本章小结

1. 本章分始时提出了一个看似简单的问题:
			程序x在机器Y上运行得有多快?

2. 不幸的是,计算机系统用来同时运行多个进程的机制使得很难获得程序性能可靠的测量值

3. 系统活动倾向于在两个不同的时间尺度上进行。
	在微观级别上,每条指令执行的时间是以ns来衡量的。
	在宏观级别上,输入/输出交互发生的延迟是以ms来衡量的。

4. 计算机系统通过不断地从一个任务切换到另一个任务来利用这种差异,
	一次运行若干计算机系统有种完全不同的记录时间流逝的方法

5. 从宏观角度来看,计时器中断(timer interrupt)发生的频率似乎很快,但是从微观的角度来看却很漫。
	通过间隔计数(interval counting)系统能够获得对程序执行时间的非常粗略的测量值。
	这种方法只对长持续时间(至少1s)的测量有用

6. 周期计数器(cycle counter)非常快,可以得到在微观尺度上很好的测量值,
	对于测量绝对时间的周期计数器,
	上下文切换的影响能够导致很小(在负载很轻的系统上〕到很大(在负载很重的系统上)的误差。

7. 因此,没有方法是完美的。
	理解在一个特殊的系统上能够获得的准确度是很重要的。

8. 取决于前面存储器引用和条件分支的历史,
	高速缓存和分支预测的影响可以导致执行代码的某个片段所需的时间每次都不同。
	通过事先运行某些将高速缓存设置为可预测状态的代码,我们可以部分地控制引起这种变化的因素,

9. 但是在有上下文切换发生时,这些尝试就没有用了。
	因此,我们必须进行多次测量,分析结果,以确定真实的执行时间。

10. 幸运的是,所有引起变化的因素的效果都是增加执行时间,
	因此只需分析确定测出的时间的最小值是否是一个准确的测量值。

11. 通过一系列的试验,我们能够设计并且验证K次最优计时方法,
	这里我们反复进行测量,直到最快的K个值都在某个互相接近的范围之内了。

12. 在一些系统上,我们能够使测量用库函数来确定时间。
	在另一些系统上,我们必须通过汇编代码来访问周期计数器
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值