几种取时间的方法(附代码)

几种取时间的方法(附代码)

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

即单调时间,跟系统的启动时间有关,不受手动修改系统时间的影响。

如上图,表示系统已经启动了24:09(8点是因为我们在东8区)。

我们用date命令将时间往回修改,观察两者的输出结果:

可以看到,指定为REAL_TIME的函数出现了时间回退(上图),指定为CLOCK_MONOTONIC的函数一直在单调流逝(下图):

表面上看,这个函数精度不错,功能完备,但却存在一个突出缺点–。对于性能敏感的函数,频繁调用会影响性能,这一点我们后面仔细说。

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 == 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_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频率是1.8Ghz的,我每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只有1.8GHz。

关于这个测试结果,还有很多内容未明确,比如time为什么这么快,gettimeofday不是有缓存么?为啥还这么慢?clock_gettime按说应该比gettimeofday慢的,怎么测试结果并非如此呢?rdtsc在各个出错场景下究竟是怎样具体的现象?上述的测试方法是否科学等。这些问题留待后续补全。

6.参考资料

  • 关于TSC的维基页面:链接
  • 读取TSC的多平台实现:链接
  • 关于TSC跨核心安全性的一个问答:链接
  • 这里是某开源库的一个实现(getTSC函数):链接

发表回复