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 uint64_t
// gcc -o time_4 time_4.c
//获取CPU频率
uint64_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;
}
//读取时间戳寄存器
uint64_t get_tsc() // TSC == Time Stamp Counter寄存器
{
#ifdef __i386__
uint64_t x;
__asm__ volatile("rdtsc" : "=A"(x));
return x;
#elif defined(__amd64__) || defined(__x86_64__)
uint64_t a, d;
__asm__ volatile("rdtsc" : "=a"(a), "=d"(d));
return (d << 32) | a;
#else // ARM架构CPU
uint32_t cc = 0;
__asm__ volatile ("mrc p15, 0, %0, c9, c13, 0":"=r" (cc));
return (uint64_t)cc;
#endif
}
int main(int argc, char **argv)
{
uint64_t cpu_freq = get_cpu_freq();
printf("cpu_freq is %lu\n", cpu_freq);
uint64_t last_tsc = get_tsc();
while(1)
{
sleep(1);
uint64_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
uint64_t get_by_time()
{
time_t tm_now;
time(&tm_now);
return tm_now;
}
uint64_t get_by_gettimeofday()
{
struct timeval tm_now;
gettimeofday(&tm_now,NULL);
return tm_now.tv_sec;
}
uint64_t get_by_clock_gettime()
{
struct timespec tm_now;
clock_gettime(CLOCK_REALTIME, &tm_now);
return tm_now.tv_sec;
}
uint64_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;
}
uint64_t get_by_tsc()
{
uint64_t a, d;
__asm__ volatile("rdtsc" : "=a"(a), "=d"(d));
return (d << 32) | a;
}
void print_diff(uint64_t loop_times, uint64_t beg_tsc, uint64_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)
{
uint64_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。