在C应用开发中我们可能经常会遇到需要知道一段代码或者一个函数运行时间的场景,本人在工作中就屡屡碰见。Linux系统提供了相应的系统调用或C库函数用于获取当前时间,因此想要获取一段代码的执行时间可以通过在代码段前后分别获取当前时间然后作差,就是代码段的运行时间了。根据这个思想,本文分别介绍Linux系统 time()、gettimeofday()函数以及 times()、clock()函数的使用来获取一段代码的执行时间。(为了方便起见,以下程序有在VS2022X86环境中调试的,也有在LINUX环境下编译调试的)
一、通过获取时间戳计算代码段执行时间
1.1 time函数
系统调用time()用于获取当前时间,以秒为单位,返回得到的值是自 1970-01-01 00:00:00 +0000 (UTC) 以来的秒数,其函数原型如下所示:
#inclue<time.h> //使用该函数需要包含头文件<time.h>
time_t time(time_t *tloc);
参数(tloc):如果tloc参数不是 NULL,则返回值也存储在 tloc 指向的内存中。
返回值:成功则返回自 1970-01-01 00:00:00 +0000 (UTC)以来的时间值(以秒为单位);失败则返回-1, 并会设置 errno。
time函数测试例程(VS2022X86环境):
#include<time.h>
#include<stdio.h>
#include <windows.h> //window环境下调用Sleep()函数包含此头文件
int main()
{
time_t start = 0;
time_t end = 0;
int i = 0,j = 0;
start = time(NULL);
if (start == -1)
{
perror("time error");
exit(-1);
}
printf("start(s) = %d\n", start);
for (i = 0; i < 20000; i++)
{
for (j = 0; j < 20000; j++)
;
}
Sleep(1000);
end = time(NULL);
if (end == -1)
{
perror("time error");
exit(-1);
}
printf("end(s) = %d\n", end);
printf("end-start = %ds\n", end-start);
return 0;
}
运行结果:
例程中不Sleep休眠1秒的运行结果:
从两个运行结果看,不加休眠测试的代码段运行时间为0,这是因为代码执行时间是非常快的,到毫秒级甚至微秒级,而time函数获取到的时间只能精确到秒,如果想要获取更加精确的时间可以使用gettimeofday函数来实现。
sleep函数在windows和linux下的不同:
windows:首字母大写,头文件包含#include <windows.h>,参数usigned long类型,为毫秒数。
linux:首字母小写,头文件包含#include <unistd.h>,参数为秒数。
1.2 gettimeofday函数
gettimeofday()函数提供微秒级时间精度,函数原型如下所示:
#include<sys/time.h> //使用该函数需要包含头文件<sys/time.h>
int gettimeofday(struct timeval *tv,struct timezone *tz);
参数(tv):参数 tv 是一个 struct timeval 结构体指针变量。
struct timeval 结构体:该结构体包含了两个成员变量 tv_sec 和 tv_usec,分别用于表示秒和微秒。
struct timeval{
long tv_sec; //秒
long tv_usec; //微秒
};
参数(tz):参数 tz 是个历史产物,早期实现用其来获取系统的时区信息,目前已遭废弃,在调用 gettimeofday() 函数时应将参数 tz 设置为 NULL。
返回值:成功返回 0;失败将返回-1,并设置 errno。
gettimeofday函数测试例程(Linux环境周立功M280主板):
#include<stdio.h>
#include<unistd.h>
#include<sys/time.h>
int main()
{
struct timeval start;
struct timeval end;
int i = 0, j = 0,ret = 0;
ret = gettimeofday(&start, NULL);
if (ret == -1)
{
perror("gettimeofday error");
exit(-1);
}
printf("start = %ds %dus\n", start.tv_sec,start.tv_usec);
for (i = 0; i < 20000; i++)
{
;
}
sleep(1);
ret = gettimeofday(&end, NULL);
if (ret == -1)
{
perror("gettimeofday error");
exit(-1);
}
printf("end = %ds %dus\n", end.tv_sec, end.tv_usec);
printf("end-start = %ds %dus\n", end.tv_sec-start.tv_sec, end.tv_usec-start.tv_usec);
return 0;
}
运行结果:
由结果可知gettimeofday函数精度可达微秒级,可根据需要选择使用time函数还是gettimeofday函数。windows系统好像并无此函数,搜索博客都是自定义函数实现该功能。
通过 time()或 gettimeofday()函数可以获取到当前时间点相对于1970-01-01 00:00:00 +0000 (UTC)这个时间点所经过时间(日历时间),得到的是一个时间段的长度,并不利于我们查看当前时间,这个结果对于我们来说非常不友好。C库中有一些时间转换函数,通过这些 API 可以将 time()或 gettimeofday()函数获取到的秒数转换为利于查看和理解的形式,比如ctime函数和localtime函数,此处不作详述。
二、通过获取进程时间计算代码段执行时间
除了上述两个函数可以通过分别在代码段前后获取时间戳相减得到代码段执行时间外,还有其他库函数通过获取进程时间同样也可以得到代码段执行时。
什么是进程时间?
进程时间指的是进程从创建后(也就是程序运行后)到目前为止这段时间内使用 CPU 资源的时间总数, 出于记录的目的,内核把 CPU 时间(进程时间)分为以下两个部分:
用户 CPU 时间:进程在用户空间(用户态)下运行所花费的 CPU 时间,有时也称为虚拟时间(virtual time)。
系统 CPU 时间:进程在内核空间(内核态)下运行所花费的 CPU 时间。这是内核执行系统调用或代表进程执行的其它任务(例如服务页错误)所花费的时间。
一般来说,进程时间指的是用户 CPU 时间和系统 CPU 时间的总和,也就是总的 CPU 时间。
2.1 times函数
times()函数用于获取当前进程时间,其函数原型如下所示:
#inclue<sys/times.h> //使用该函数需要包含头文件<sys/times.h>
clock_t times(struct tms *buf);
参数(buf):times()会将当前进程时间信息存在一个 struct tms 结构体数据中,所以需要提供struct tms变量,使用参数 buf 指向该变量。
返回值:返回值类型为 clock_t(实质是 long 类型),调用成功将返回从过去任意的一个时间点(譬如系统启动时间)所经过的时钟滴答数(其实就是系统节拍数),将(节拍数 / 节拍率)便可得到秒数, 返回值可能会超过 clock_t 所能表示的范围(溢出);调用失败返回-1,并设置 errno。
struct tms 结构体:
struct tms {
clock_t tms_utime; // user time, 进程的用户CPU时间, tms_utime个系统节拍数
clock_t tms_stime; //system time, 进程的系统 CPU 时间, tms_stime 个系统节拍数
clock_t tms_cutime; //user time of children, 已死掉子进程的 tms_utime + tms_cutime 时间总和
clock_t tms_cstime; //system time of children, 已死掉子进程的 tms_stime + tms_cstime 时间总和
};
times函数测试例程(Linux环境周立功M280主板):
#include<sys/times.h>
#include<stdio.h>
#include<unistd.h>
int main()
{
struct tms start;
struct tms end;
clock_t t_start =0;
clock_t t_end =0;
long tck = 0;
int i = 0, j = 0;
tck = sysconf(_SC_CLK_TCK); //调用sysconf函数获取系统的节拍率,需包含<unistd.h>头文件
t_start = times(&start);
if(-1==t_start)
{
perror("times error");
exit(-1);
}
for (i = 0; i < 20000; i++)
{
for(j = 0; j < 2000; j++)
;
}
//sleep(1);
t_end = times(&end);
if(-1==t_end)
{
perror("times error");
exit(-1);
}
printf("时间总和 = %f秒\n",(t_end-t_start)/(double)tck);
printf("用户CPU时间 = %f秒\n",(end.tms_utime-start.tms_utime)/(double)tck);
printf("系统CPU时间 = %f秒\n",(end.tms_stime-start.tms_stime)/(double)tck);
return 0;
}
程序中使用 sysconf(_SC_CLK_TCK)获取到系统节拍率,sysconf函数的原型是long sysconf(int name);参数name有很多宏定义类型可分别获取相应的系统配置信息,例如页大小、最大页数、cpu个数、一个进程可同时打开的文件最大数等,此处不作详述需要了解可网上查阅。start和end参数用来存放当前进程时间,若只需要获取总时间用返回值即可,若想要获取用户CPU时间和系统CPU时间则需要用到参数值。
运行结果:
sleep休眠1s运行结果:
系统 CPU 时间为 0 秒,说明测试的这段代码并没有进入内核态运行(不过有时候系统CPU时间为0.01秒,原因目前本人还不清楚)。系统 CPU 时间为 0 秒,也就是说测试的这段代码并没有进入内核态运行。时间总和并不是总的进程时间,指的是从起点到终点所经过的时间,时间总和包括了进程处于休眠状态时消耗的时间(sleep 等会让进程挂起、进入休眠状态),图5结果可以发现时间总和比进程时间多 1 秒,其实这1秒就是进程处于休眠状态的时间。
Tips:进程时间不等于程序的整个生命周期所消耗的时间,如果进程一直处于休眠状态(进程被挂起、 不会得到系统调度),那么它并不会使用 CPU 资源,所以休眠的这段时间并不计算在进程时间中。
2.2 clock函数
库函数clock()提供了一个更为简单(之所以说它简单,个人以为应该是没有复杂的参数吧)的方式用于获取进程时间,它的返回值描述了进程使用的总的 CPU 时间(也就是进程时间,包括用户 CPU 时间和系统 CPU 时间,不能获取单独的两个时间),其函数原型如下所示:
#include<time.h> //使用该函数需要包含头文件<time.h>
clock_t clock(void);
返回值:返回值是到目前为止程序的进程时间,为 clock_t 类型,注意 clock()的返回值并不是系统节拍数,如果想要获得秒数,请除以CLOCKS_PER_SEC(这是一个宏)。如果返回的进程时间不可用或其值无法表示,则返回值是-1。
测试例程(VS2022X86环境):
#include<time.h>
#include<stdio.h>
#include<stdlib.h>
#include<windows.h>
int main()
{
clock_t start =0;
clock_t end =0;
long tck = 0;
int i = 0, j = 0;
start = clock();
if (-1 == start)
{
exit(-1);
}
for (i = 0; i < 20000; i++)
for (j = 0; j < 20000; j++)
;
//Sleep(1000);
end = clock();
if (-1 == start)
{
exit(-1);
}
printf("%f\n", (double)CLOCKS_PER_SEC);
printf("%d\n", end - start);
printf("总的CPU时间 = %f\n", (end - start) / (double)CLOCKS_PER_SEC);
return 0;
}
运行结果:
本人测试环境中CLOCKS_PER_SEC的定义为:#define CLOCKS_PER_SEC ((clock_t)1000),说明clock函数的返回值单位为毫秒,不过有资料表示:在不同的编译环境中,CLOCKS_PER_SEC的数值可能是不同的,甚至还有被定义为1000000的。因此使用此函数需要特别注意一下。