Linux用户态程序计时方式详解(1)


前言

良好的计时器可帮助程序开发人员确定程序的性能瓶颈,或对不同算法进行性能比较。但要精确测量程序的运行时间并不容易,因为进程切换、中断、共享的多用户、网络流量、高速缓存访问及转移预测等因素都会对程序计时产生影响。

本文将不考虑这些影响因素(相关资料可参考《深入理解计算机系统》一书),而仅仅关注Linux系统中用户态程序执行时间的计算方式。除本文所述计时方式外,还可借助外部工具统计耗时,如《Linux调试分析诊断利器——strace》一文中介绍的strace。

本文示例代码的运行环境如下:

 

一  基本概念

1.1 日历时间

Coordinated Universal Time(UTC):世界协调时间(又称世界标准时间),旧称格林威治标准时间(Greenwich Mean Time, GMT)。

Calendar Time:日历时间,即从一个标准时间点到此时的时间所经过的秒数。该标准时间点因编译器而异,但对编译系统而言标准时间点不变。该编译系统中的时间对应的日历时间都通过该标准时间点衡量,故日历时间是“相对时间”。UNIX/Linux的时间系统由“新纪元时间(Epoch)”开始算起,该起点指定为1970年1月1日凌晨0时0分0秒(格林威治时间)。Microsoft C/C++ 7.0中标准时间点指定为1899年12月31日0时0分0秒,而其它版本的Microsoft C/C++和所有不同版本的Visual C++中标准时间点指定为1970年1月1日0时0分0秒。日历时间与时区无关。

Epoch:时间点。时间点在标准C/C++中是一个整数(time_t),它用此刻的时间和标准时间点相差的秒数(即日历时间)来表示。目前大部分UNIX系统采用32位记录时间,正值表示为1970年以后,负值则表示1970年以前。可简单地估算出所能表达的时间范围:1970±((231-1)/3600/24/365)≈[1901,2038]年。为表示更久远的时间,某些编译器厂商引入64位甚至更长的整型数来保存日历时间。

1.2 进程时间

进程时间也称CPU时间,用以度量进程使用的中央处理器资源。进程时间以时钟滴嗒计算,通常使用三个进程时间值,即实际时间(Real)、用户CPU时间(User)和系统CPU时间(Sys)。

实际时间指实际流逝的时间;用户时间和系统时间指特定进程使用的CPU时间。具体区别如下:

  • Real是从进程开始执行到完成所经历的挂钟(wall clock)时间,包括其他进程使用的时间片(time slice)和本进程耗费在阻塞(如等待I/O操作完成)上的时间。该时间对应秒表(stopwatch)直接测量。
  • User是进程执行用户态代码(内核外)耗费的CPU时间,仅统计该进程执行时实际使用的CPU时间,而不计入其他进程使用的时间片和本进程阻塞的时间。
  • Sys是该进程在内核态运行所耗费的CPU时间,即内核执行系统调用所使用的CPU时间。

CPU总时间(User+Sys)是CPU执行用户进程操作和内核(代表用户进程执行)系统调用所耗时间的总和,即该进程(包括其线程和子进程)所使用的实际CPU时间。若程序循环遍历数组,则增加用户CPU时间;若程序执行exec或fork等系统调用,则增加系统CPU时间。

在多核处理器机器上,若进程含有多个线程或通过fork调用创建子进程,则实际时间可能小于CPU总时间——因为不同线程或进程可并行执行,但其时间会计入主进程的CPU总时间。若程序在某段时间处于等待状态而并未执行,则实际时间可能大于CPU总时间。其数值关系总结如下:

  • Real < CPU,表明进程为计算密集型(CPU bound),利用多核处理器的并行执行优势;
  • Real ≈ CPU,表明进程为计算密集型(CPU bound),未并行执行;
  • Real > CPU,表明进程为I/O密集型(I/O bound),多核并行执行优势并不明显。

在单核处理器上,Real时间和CPU时间之差,即Real- (User + Sys)是所有延迟程序执行的因素的总和。可估算程序运行期间的CPU利用率为CpuUsage = (User + Sys)/ Real * 100(%)。

在SMP(对称多处理系统)上,该差值近似为Real* ProcessorNum - (User + Sys)。这些因素包括:

  • 调入程序文本和数据的I/O操作;
  • 获取程序实际使用内存的I/O操作;
  • 由其它程序消耗的CPU用时;
  • 由操作系统消耗的CPU用时。

二  计时方式

本节将基于下面的函数来讨论和对比各种计时方式:

1 #include <math.h>
2 #define TIME_LOOP_NUM    1000000*20
3 void TimingFunc(void){
4unsigned int i = 0;
5     double y = 0.0;
6     for(; i < TIME_LOOP_NUM; i++)
7         y = sin((double)i);
8 }

2.1 间隔计数

操作系统用计时器(timer)来记录每个进程使用的累计时间,该时间只是程序执行时间的粗略测量值。

操作系统维护着每个进程使用的用户时间量和系统时间量的计数值。当计时器中断发生时,操作系统会在当前进程列表中寻找活动的进程,并对该进程的计数值增加计时器时间间隔(通常10毫秒)。若该进程在内核模式中运行,则增加系统时间,否则增加用户时间。

这种间隔计数(“记账”)方法原理虽然简单但并不精确。若某进程运行时间很短(与系统计时器间隔相同数量级),且计时中断发生时发现进程正在运行,则不论进程已运行一段时间还是中断前1毫秒才开始运行,都会对计数器增加计时器时间间隔;中断发生时进程已切换的情况与之类似。因此,间隔计数时头尾都有误差。不过,若程序运行时间足够长(至少数秒),间隔计数的不准确性可能相互弥补(高估和低估的测量值平均后误差接近0)。理论上很难分析该误差值,故通常只有程序运行时间达到秒级时,采用间隔计数方法才有意义。此外,该方法的主要优点是其准确性不是非常依赖于系统负载。

Linux系统time命令和times库函数采用间隔计数方法测量命令或程序执行时间。

2.1.1 time命令

time命令可测量命令或脚本执行所耗时间及系统资源使用等信息,统计结果包含以下时间(以秒计):

  • 实际执行时间(real time):从命令行执行到运行结束所消耗的时间;
  • 用户CPU时间(user CPU time):命令在用户态中执行所消耗的CPU时间,即程序本身及其调用的库函数所使用的时间;
  • 系统CPU时间(system CPU time):命令在内核态中执行所消耗的CPU时间,即由程序直接或间接调用的系统调用执行的时间。

Linux系统中,可使用Shell内置命令time,或GNU一般命令time(/usr/bin/time)来测试程序运行的时间。前者只负责计时,精度可达10毫秒;后者精度略低,但可访问getrusage系统调用的信息,并提供丰富的参数选项,包括指定输出文件等功能。

time命令不能用于测量程序内某个函数或某段代码的执行时间。

2.1.1.1 Shell命令

Shell内置命令time的使用格式为

time <command> [<arguments...>]

命令行执行完成后,会在标准输出中打印执行该命令行的时间统计结果。例如:

 

可见Real>(User+Sys),说明处理器可能同时在执行其他进程,或本进程被阻塞或睡眠(sleep)。睡眠时间不计入用户时间和系统时间。阻塞可能是因为系统调用的错误使用,也可能是系统中的慢设备引起的。

又如统计在当前目录下查找文件hello.c所消耗的时间:

 

可见Real远大于(User+Sys),因为find命令遍历各个目录时进行大量磁盘I/O操作,这些操作比较耗时,因此大部分时间find进程都在等待磁盘I/O完成。此外,与文件相关的系统调用也会消耗系统时间。

再次运行find命令时,real时间将显著减小:

 

这得益于系统文件缓存,磁盘I/O操作次数显著减少。

以下两种方法可将time命令输出的时间信息重定向到文件里,如下所示:

{ time find . -name "hello.c"; } 2>hello.txt  //代码块(花括号内侧空格符不可少)
(time find . -name "hello.c") 2>hello.txt     //子Shell(多占些资源)

注意上面示例中的花括号和小括号不可缺少,否则Shell会把time关键字后面的命令行作为一个整体进行处理,time命令本身的输出不会被重定向。内置命令time输出到标准错误,文件描述符2表示标准错误stderr。若还要包括find命令执行的结果,则可用:

(time find . -name "hello.c") 2>hello.txt 2>&1


相关内容