1.上古版
最原始的取时间的方法大概就是time+localtime了,见代码:
-
#include
<stdio.h
>
-
#include
<
time.h
>
-
-
/
/ gcc -o
time_
1
time_
1.c
-
-
int main()
-
{
-
time_t tm_now;
-
-
time(
&tm_now);
/
/ 或者写成 tm_now
=
time(
NULL);
-
-
/
/
1.直接打印:
1970-
1-
1,00:
00:
00到现在的秒数
-
printf(
"now time is %ld second\n", tm_now);
-
-
/
/
2.转换成本地时间,精确到秒
-
struct tm
*p_local_tm ;
-
p_local_tm
= localtime(
&tm_now) ;
-
printf(
"now datetime: %04d-%02d-%02d %02d:%02d:%02d\n",
-
p_local_tm-
>tm_year
+
1900,
-
p_local_tm-
>tm_mon
+
1,
-
p_local_tm-
>tm_mday,
-
p_local_tm-
>tm_hour,
-
p_local_tm-
>tm_min,
-
p_local_tm-
>tm_sec);
-
-
return
0;
-
}
其中time函数返回的是1970年到现在的秒数,精确到秒。
localtime函数是根据这个秒数和本机的时区,解析出年月日时分秒等信息。
这里特别提醒一点,localtime函数不是多线程安全的,localtime_r才是。
还要特别提醒一点,不要在信号响应函数中使用localtime或localtime_r,程序会卡死!
程序运行结果如下:
2.傻瓜版
另一个比较好用的函数是gettimeofday。
相比其他函数,gettimeofday可以精确到微秒,还可以指定时区,性能也还可以,可以满足绝大多数场景,因此叫傻瓜版。
示例代码如下:
-
#include
<stdio.h
>
-
#include
<sys
/
time.h
>
-
#include
<
time.h
>
-
-
/
/ gcc -o
time_
2
time_
2.c
-
-
int main()
-
{
-
struct timeval tm_now;
-
-
/
/
1.获取当前时间戳(tv_sec, tv_usec)
-
gettimeofday(
&tm_now,
NULL);
/
/ 第二个参数是时区
-
-
/
/
2.转换成本地时间,精确到秒
-
struct tm
*p_local_tm;
-
p_local_tm
= localtime(
&tm_now.tv_sec) ;
-
printf(
"now datetime: %04d-%02d-%02d %02d:%02d:%02d.%06ld\n",
-
p_local_tm-
>tm_year
+
1900,
-
p_local_tm-
>tm_mon
+
1,
-
p_local_tm-
>tm_mday,
-
p_local_tm-
>tm_hour,
-
p_local_tm-
>tm_min,
-
p_local_tm-
>tm_sec,
-
tm_now.tv_usec);
/
/ 有微秒时间戳了
-
-
return
0;
-
}
运行结果如下:
3.进阶版
如果微秒级别的精度还不满足要求,可以尝试下clock_gettime,代码如下:
-
#include
<stdio.h
>
-
#include
<unistd.h
>
-
#include
<
time.h
>
-
-
/
/ gcc -o
time_
3
time_
3.c
-
-
void print_timestamp(int
use_monotonic)
-
{
-
struct timespec tm_now;
-
-
/
/
1.获取当前时间戳(tv_sec, tv_usec)
-
if(
use_monotonic)
-
clock_gettime(CLOCK_MONOTONIC,
&tm_now);
/
/ 单调时间,屏蔽手动修改时间
-
else
-
clock_gettime(CLOCK_REALTIME,
&tm_now);
/
/ 机器时间
-
-
/
/
2.转换成本地时间,精确到秒
-
struct tm
*p_local_tm;
-
p_local_tm
= localtime(
&tm_now.tv_sec) ;
-
printf(
"now datetime: %04d-%02d-%02d %02d:%02d:%02d.%09ld\n",
-
p_local_tm-
>tm_year
+
1900,
-
p_local_tm-
>tm_mon
+
1,
-
p_local_tm-
>tm_mday,
-
p_local_tm-
>tm_hour,
-
p_local_tm-
>tm_min,
-
p_local_tm-
>tm_sec,
-
tm_now.tv_nsec);
/
/ 有纳秒时间戳了
-
}
-
-
int main(int argc, char
**argv)
-
{
-
int
use_monotonic
=
0;
-
-
int optval
=
0;
-
while ((optval
= getopt(argc, argv,
"Mm")) !
= EOF)
-
{
-
switch (optval)
-
{
-
case
'M':
-
case
'm':
-
use_monotonic
=
1;
-
break;
-
default:
-
break;
-
}
-
}
-
-
while(
1)
-
{
-
print_timestamp(
use_monotonic);
-
sleep(
1);
-
}
-
-
return
0;
-
}
运行结果如下:
clock_gettime的第一个参数可以指定一个clock_id参数:
常见的有两个:
1) CLOCK_REALTIME
即普通的时间,跟其他时间函数取出来的时间并无区别,运行效果如上。
2) CLOCK_MONOTONIC
即单调时间,跟系统的启动时间有关,不受手动修改系统时间的影响。
如上图,表示系统已经启动了6 05:47:53(东8区零点是1970-01-01 08:00:00)。
表面上看,这个函数精度不错,功能完备,但却存在一个突出缺点–慢。对于性能敏感的函数,频繁调用会影响性能,这一点我们后面仔细说。
4.专家版
专家版本的计时函数有两个突出优点:
- 性能高:绕过内核直接读寄存器,开销很小
- 精度高:时间测量的最小单位是1/CPU频率秒,可达0.3纳秒(假设CPU频率为3GHz)
下面是示例程序:
-
-
#include
<stdio.h
>
-
#include
<unistd.h
>
-
#include
<stdlib.h
>
/
/
for atof
-
#include
<stdint.h
>
/
/
for uint
64_t
-
-
/
/ gcc -o
time_
4
time_
4.c
-
-
/
/获取CPU频率
-
uint
64_t
get_cpu_freq()
-
{
-
FILE
*fp
=popen(
"lscpu | grep CPU | grep MHz | awk {'print $3'}",
"r");
-
if(fp
=
= nullptr)
-
return
0;
-
-
char cpu_mhz_str[
200]
= {
0 };
-
fgets(cpu_mhz_str,
80,fp);
-
fclose(fp);
-
-
return atof(cpu_mhz_str)
*
1000
*
1000;
-
}
-
-
/
/读取时间戳寄存器
-
uint
64_t
get_tsc()
/
/ TSC
=
=
Time Stamp Counter寄存器
-
{
-
#ifdef __i
386__
-
uint
64_t x;
-
__asm__ volatile(
"rdtsc" :
"=A"(x));
-
return x;
-
#elif defined(__amd
64__) || defined(__x
86_
64__)
-
uint
64_t a, d;
-
__asm__ volatile(
"rdtsc" :
"=a"(a),
"=d"(d));
-
return (d
<
<
32) | a;
-
#
else
/
/ ARM架构CPU
-
uint
32_t cc
=
0;
-
__asm__ volatile (
"mrc p15, 0, %0, c9, c13, 0":
"=r" (cc));
-
return (uint
64_t)cc;
-
#endif
-
}
-
-
int main(int argc, char
**argv)
-
{
-
uint
64_t cpu_freq
=
get_cpu_freq();
-
printf(
"cpu_freq is %lu\n", cpu_freq);
-
-
uint
64_t
last_tsc
=
get_tsc();
-
while(
1)
-
{
-
sleep(
1);
-
uint
64_t cur_tsc
=
get_tsc();
-
printf(
"TICK(s) : %lu\n", cur_tsc
-
last_tsc);
-
printf(
"Second(s) : %.02lf\n",
1.0
* (cur_tsc
-
last_tsc)
/ cpu_freq);
-
last_tsc
= cur_tsc;
-
}
-
return
0;
-
}
TSC的全称是Time Stamp Counter,它是一个保存着CPU运转时钟周期数的寄存器,在X86等平台下均有提供(ARM平台下是CCR-Cycle Counter Register)。
通过专门的rdtsc汇编指令,可绕过操作系统内核直接从寄存器中读取数值,因此速度极快。
通过上述的get_tsc函数可以从这个寄存器中读出一个64位的数值,连续两次读取的值的差值,即是连续两次调用之间CPU运行的周期数。用这个周期数除以CPU运行的频率(通过上面的get_cpu_freq函数获得),即可得到具体的秒数。
上述代码运行效果如下:
可以看到,我测试用的机器的CPU频率是2.9Ghz的,我每sleep一秒输出一下两次CPU计数器的差值,发现跟频率也能对的上。
事实上,上面的所有取时间的函数,都是基于底层的类似rdtsc指令封装的,我们直接使用最底层的命令,固然快且精确,但是也不可避免的要直面一些坑。
比如我们可能碰见多CPU问题、多线程问题、进程上下文切换问题,计算机主动调节CPU频率问题等。为了顺利地使用这个指令,我们就要对程序和操作系统做一系列的限制,比如rdtsc的结果不在CPU间共享、进程运行时绑定CPU以避免被切换到另外的CPU上去、禁止计算机主动调频功能等。
5.关于性能
我们写了一个测试程序,跑10亿次,取平均时间,分别测试几个函数的性能:
-
#include
<stdio.h
>
-
#include
<unistd.h
>
-
#include
<stdlib.h
>
-
#include
<stdint.h
>
-
#include
<
time.h
>
-
#include
<sys
/
time.h
>
-
-
/
/ gcc -o
time_
5
time_
5.c
-
-
uint
64_t
get_
by_
time()
-
{
-
time_t tm_now;
-
time(
&tm_now);
-
return tm_now;
-
}
-
-
uint
64_t
get_
by_gettimeofday()
-
{
-
struct timeval tm_now;
-
gettimeofday(
&tm_now,
NULL);
-
return tm_now.tv_sec;
-
}
-
-
uint
64_t
get_
by_clock_gettime()
-
{
-
struct timespec tm_now;
-
clock_gettime(CLOCK_REALTIME,
&tm_now);
-
return tm_now.tv_sec;
-
}
-
-
uint
64_t
get_cpu_freq()
-
{
-
FILE
*fp
=popen(
"lscpu | grep CPU | grep MHz | awk {'print $3'}",
"r");
-
if(fp
=
=
NULL)
-
return
0;
-
-
char cpu_mhz_str[
200]
= {
0 };
-
fgets(cpu_mhz_str,
80,fp);
-
fclose(fp);
-
-
return atof(cpu_mhz_str)
*
1000
*
1000;
-
}
-
-
uint
64_t
get_
by_tsc()
-
{
-
uint
64_t a, d;
-
__asm__ volatile(
"rdtsc" :
"=a"(a),
"=d"(d));
-
return (d
<
<
32) | a;
-
}
-
-
void print_diff(uint
64_t loop_
times, uint
64_t beg_tsc, uint
64_t
end_tsc)
-
{
-
double tt_ns
= (
end_tsc
- beg_tsc)
*
1.0
*
1000
*
1000
*
1000
/
get_cpu_freq();
-
-
printf(
"Number Loop : %lu\n", loop_
times);
-
printf(
"Total Time : %.02lf ns\n", tt_ns);
-
printf(
"Avg Time : %.02lf ns\n", tt_ns
/ loop_
times);
-
}
-
-
#define LOOP_
TIMES
1000000000
-
-
int main(int argc, char
**argv)
-
{
-
uint
64_t beg_tsc,
end_tsc;
-
long loop;
-
-
printf(
"-------------time()-------------\n");
-
loop
= LOOP_
TIMES;
-
beg_tsc
=
get_
by_tsc();
-
while(loop--)
-
get_
by_
time();
-
end_tsc
=
get_
by_tsc();
-
print_diff(LOOP_
TIMES, beg_tsc,
end_tsc);
-
-
printf(
"-------------gettimeofday()-------------\n");
-
loop
= LOOP_
TIMES;
-
beg_tsc
=
get_
by_tsc();
-
while(loop--)
-
get_
by_gettimeofday();
-
end_tsc
=
get_
by_tsc();
-
print_diff(LOOP_
TIMES, beg_tsc,
end_tsc);
-
-
printf(
"-------------clock_gettime()-------------\n");
-
loop
= LOOP_
TIMES;
-
beg_tsc
=
get_
by_tsc();
-
while(loop--)
-
get_
by_clock_gettime();
-
end_tsc
=
get_
by_tsc();
-
print_diff(LOOP_
TIMES, beg_tsc,
end_tsc);
-
-
printf(
"-------------rdtsc-------------\n");
-
loop
= LOOP_
TIMES;
-
beg_tsc
=
get_
by_tsc();
-
while(loop--)
-
get_
by_tsc();
-
end_tsc
=
get_
by_tsc();
-
print_diff(LOOP_
TIMES, beg_tsc,
end_tsc);
-
-
return
0;
-
}
测试结果如下:
可以看到:
- time函数最快,但是精度太低
- gettimeofday和clock_gettime虽然精度高,但是都比较慢
- rdtsc精度和速度都十分优秀
另外需要注意一点的是,上述测试结果跟机器配置有很大关系,我测试所用的机器是一台ubuntu虚拟机,CPU只有2.9GHz。