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。