习惯用的使用c-style的时间, 导致有时候在cpp程序中看着很别扭.
网上的有些文章看的实在是太敷衍, 索性自己写一篇吧.
引子
本文以如下方式展开:
- 时间相关的概念
- Linux C中常用时间编程方式和手段
- 现代C++时间编程的方式和惯例
(代码, 命令环境Ubuntu 14)
正文
相关概念
UTC
Coordinated Universal Time, 即GMT(Greenwich Mean Time); 把格林威治标准时间作为标准时间.
比如, 中国内地的时间与UTC的时差为+8, 也就是UTC+8, 美国是UTC-5.
CST
CST: 中部标准时间, 这个时间是要分时区的. 一般看到的是:
- CST China Standard Time
- CST Cuba Standard Time
1 | $ date |
DST
Daylight Saving Time, 又称“日光节约时制”和“夏令时间”,是一种为节约能源而人为规定地方时间的制度,在这一制度实行期间所采用的统一时间称为“夏令时间”。一般在天亮早的夏季人为将时间调快一小时,可以使人早起早睡,减少照明量,以充分利用光照资源,从而节约照明用电。
根据时区不同, 也不有不同算法调整, 具体可以参考man手册 man gettimeofday
.1
2
3
4
5
6
7
int gettimeofday(struct timeval *tv, struct timezone *tz); //第二个参数一般传入NULL
struct timezone {
int tz_minuteswest; /* minutes west of Greenwich */
int tz_dsttime; /* type of DST correction */
};
Epoch
1970年1月1日0:0:0 UTC(世界标准时间)
相当于计时起始时间.
CalendarTime
日历时间
是用 “从一个标准时间点到此时的时间经过的秒数” 来表示的时间, 它是一个相对时间. (距离时间起点经过的时间)
这个标准时间点对不同的编译器来说会有所不同, 但对一个编译系统来说, 这个标准时间点是不变的, 该编译系统中的时间对应的日历时间都通过该标准时间点来衡量, 所以可以说日历时间是”相对时间”, 但是无论你在哪一个时区, 在同一时刻对同一个标准时间点来说, 日历时间都是一样的.
下面的命令可以查看自1970年1月1日0:0:0 UTC(世界标准时间)到现在的秒数:1
$ date +%s
然后把这些时间按照我们的年月日换算就可以算出现在的日历时间:1
$ date -d @秒数
ClockTick
时钟计时单元(而不把它叫做时钟滴答次数), 一个 clock tick
不是 CPU 的一个时钟周期, 而是系统的一个基本计时单位(时钟中断的间隔时长).
Second
时间单位:
- millisecond 毫秒 ms 1e-3秒
- microsecond 微秒 us 1e-6秒
- nanosecond 纳秒 ns 1e-9秒
- picosecond 皮秒 ps 1e-12秒
我一般记忆到纳秒:
1秒是1000毫秒, 100万微妙, 10亿纳秒.
下面总结编程中需要用的时间内容, 一般是为了
- 具体某个函数调用
- 统计进程运行时间
- 获取系统时间
- profile/性能测试
C部分
在Linux C下有两个重要的头文件, time.h
, sys/time.h
, 主要还是使用 time.h
.
结构体定义
常用的结构体或定义:
- time_t 统计秒数(注意范围)
- clock_t 时钟单位值(一般用于统计程序运行时间)
- timeval 时间间隔(一般用于gettimeofday, 定时器之类函数参数上)
- tm 正规时间格式, 分解时间(broken-down time)
你不要关心具体的实现或者定义, 但是知道最好(可能你需要在time_t和tm之间进行转换).
下面说一下: (环境Ubuntu 64)
time_t的定义:
1
2
3
4
5
6__STD_TYPE __TIME_T_TYPE __time_t; /* Seconds since the Epoch. */
//微妙suseconds_t 也是long int(他和tm结构体完全不一样)一看就是记录的当前时间秒数, 但是各个编译器厂商记录该时间的定义可能不太一样, 在Visual C++中采用了__time64_t数据类型来保存日历时间, 相应的通过
_time64()
来获取日历秒数(这样表示的大小更大了)timeval的定义: (
man gettimeofday
)1
2
3
4
5
6
7
8
9struct timeval {
time_t tv_sec; /* seconds */
suseconds_t tv_usec; /* microseconds */
};
struct timezone {
int tz_minuteswest; /* minutes west of Greenwich */
int tz_dsttime; /* type of DST correction */
};如果你使用
则要手动加宏: 1
2不加宏, 直接用
1
小的测试:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
int main(void)
{
struct timeval tv;
gettimeofday(&tv, NULL);
printf("%s \n", ctime(&tv.tv_sec) );
return 0;
}tm 的定义
1
2
3
4
5
6
7
8
9
10
11struct tm {
int tm_sec; /* seconds */
int tm_min; /* minutes */
int tm_hour; /* hours */
int tm_mday; /* day of the month */
int tm_mon; /* month */
int tm_year; /* year */
int tm_wday; /* day of the week */
int tm_yday; /* day in the year */
int tm_isdst; /* daylight saving time */
};值得注意的是, glic可能有自己的实现, 也就是说还有更多的字段. 其他详细信息可以参考
man ctime
里的说明.clock_t 定义:
1
2
3
4
typedef long clock_t;还定义了一秒有多少个
时钟单元
:1
函数定义
定义在 time.h
中的重要函数:
- extern clock_t clock(time_t *__timer); //查看进程运行时间
- extern time_t time(time_t *__timer); //获取当前系统时间(你拿到的是秒数)
- extern char ctime(__const time_t __timer); //把秒数转换成当前时间格式字符串
- extern char asctime(const struct tm tm); //作用同上, 参数不同
- extern struct tm gmtime(__const time_t __timer);//作用同上, 不过返回是UCT格式结构体
- extern struct tm localtime(const time_t __timer); //作用同上, 不过是当前时区格式结构体
- extern time_t mktime(struct tm *__time); //得到当前时间秒数
- extern double difftime(time_t time1, time_t time0); //计算时间差, 可以自己算
还有一个提取 struct tm
时间结构体某一项的函数:(类似 sprintf 这种格式化输出)1
2
3
4
5
6//特别注意, 这个函数返回的 buf 没有\0结尾.
//并且在存储相关内容之前, 长度不够, 那么返回值为0, 且buf未定义.
extern size_t strftime(char *buf,
size_t max,
const char *format,
const struct tm *tm);
format 的取值如下:
1 | /* |
一个小demo: (12进制显示当前时间)1
2
3
4
5
6
7char str[100] = {0};
//memset(str, 0, 100);
struct tm *tm_ptr = localtime(time(NULL));
strftime(str, 100, "It is now : %I %p", pt);
str[strlen(str)] = '\0';
//输出str
printf("%s\n", str);
注意:
注意
clock()
函数, 返回的是usr time + system time
, 即用户时间+内核时间, 返回的时间(始终计数器值)需要转换成秒, 可以用下面的公式:1
2
3
4
5
6//result / CLOCKS_PER_SECOND; //1毫秒, 时钟计数器+1, 即调用clock()函数返回的值就加1
void elapsed_time()
{
printf("Elapsed time:%u secs./n", clock()/CLOCKS_PER_SEC);
}当然你可以计算某一个调用, 某一个循环化了多长时间:(但是精度到毫秒)
1
2
3
4
5
6
7long i = 10000000L; //把i减至0花了多长时间
clock_t start = clock();
while( i-- );
clock_t finish = clock();
double duration = (double)(finish - start) / CLOCKS_PER_SEC;asctime
和ctime
将时间以固定的格式显示出来,两者的返回值都是char*型的字符串, 但是参数不同- 注意
gmtime()
以及localtime
是可以把 time_t 转换成 tm 的; 这两个函数涉及到了tm, 也就不仅仅只有时间, 还有日期了 - 注意
mktime()
可以把 tm 当前时间格式, 转换成 time_t 当前时间秒数 - 注意
time
拿到的是当前时间的秒数, 见到某平台的库实现_time64()
也不必惊慌
完整的定义可以查看 man 手册:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
char *asctime(const struct tm *tm);
char *asctime_r(const struct tm *tm, char *buf);
char *ctime(const time_t *timep);
char *ctime_r(const time_t *timep, char *buf);
struct tm *gmtime(const time_t *timep);
struct tm *gmtime_r(const time_t *timep, struct tm *result);
struct tm *localtime(const time_t *timep);
struct tm *localtime_r(const time_t *timep, struct tm *result);
time_t mktime(struct tm *tm);
/*--------------------------------------------------------------*/
size_t strftime(char *s, size_t max, const char *format,
const struct tm *tm);
定义在sys/time.h中的重要函数1
2
3
4
int gettimeofday(struct timeval *tv, struct timezone *tz);
int settimeofday(const struct timeval *tv, const struct timezone *tz);
综合案例
1 |
|
Cpp部分
已经看到了, 如果你去使用C库 time.h
, 那么时间操作都是够用的; 但是在C++里一般用 ctime
, 如下图:
其实是没有差距的, 如果你要用 ctime
这个头文件的话(注意, 加上命名空间 std::
).
但是C++不仅仅包含 C-style date and time library
, 这里还从Boost库里引入了 chrono
库:
The chrono library, a flexible collection of types that track time with varying degrees of precision (e.g. std::chrono::time_point)
chrono库包含3个内容:
- durations (std::chrono::duration类模板, 就相当于一个自带时间单位的时间间隔)
- clocks (three clock types)
- system_clock类 (该类的is_steady属性为false, 会随着系统时间而改变)
- steady_clock类 (不会因为你修改系统时间而改变)
- high_resolution_clock类 (一般是上面两种的宏定义, 具体看平台)
- time points (time_point类模板)–时间点
其中比较有用的就是 system_clock, 相当于原来clock()函数扩展成了一个类, 和 time_point 模板类(可以以system_lock时钟为计时单位参数), 以及计算时间价格的方法的封装类模板 duration. (其实你可以看到, 都是原来时间函数的封装, 以面向对象的姿态展现)
使用起来, 真的是太方便了, 下面给出一个简单的综合案例:
(统计函数调用时间的, 和我们上面c库中clock用法类似)1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
long fibonacci(unsigned n)
{
if (n < 2) return n;
return fibonacci(n-1) + fibonacci(n-2);
}
int main()
{
/*
//原来的写法
clock_t start = clock();
while( i-- );
clock_t finish = clock();
double duration = (double)(finish - start) / CLOCKS_PER_SEC;
*/
//using namespace std::chrono;
//以system_clock为计时类型的时间点
//time_point<system_clock> start, end;
std::chrono::time_point<std::chrono::system_clock> start, end;
//start = system_clock::now();
start = std::chrono::system_clock::now();
std::cout << "f(42) = " << fibonacci(42) << std::endl;
end = std::chrono::system_clock::now();
std::chrono::duration<double> elapsed_seconds = end-start;
//time_point 转换成 time_t
std::time_t end_time = std::chrono::system_clock::to_time_t(end);
std::cout << "finished computation at " << std::ctime(&end_time)
<< "elapsed time: " << elapsed_seconds.count() << "s\n";
}
下面一个个仔细说:
duration
std::chrono::duration
表示一段时间间隔, 用来记录时间长度(这个时候你可以在模板参数里指定计时单位, 默认是秒), 其原型如下:1
template<class Rep, class Period = std::ratio<1>> class duration;//默认秒做时间单位
- 第一个模板参数 Rep 是一个数值类型(一般是int, 多少秒, 分等),表示时钟个数;
第二个模板参数是一个默认模板参数std::ratio,它的原型是:
1
template<std::intmax_t Num, std::intmax_t Denom = 1> class ratio;
它表示每个时钟周期的秒数, 其中第一个模板参数 Num 代表分子, Denom代表分母, 分母默认为1, ratio代表的是一个分子除以分母的分数值, 比如
ratio<2>
代表一个时钟周期是两秒, ratio<60>代表了一分钟, ratio<60*60>代表一个小时, ratio<60*60*24>代表一天. 而ratio<1, 1000="">代表的则是1/1000秒即一毫秒, ratio<1, 1000000="">代表一微秒, ratio<1, 1000000000="">代表一纳秒.
标准库为了方便使用, 就定义了一些常用的时间间隔, 如时、分、秒、毫秒、微秒和纳秒, 在chrono命名空间下, 它们的定义如下:1,>1,>1,>60*60*24>60*60>60>1
2
3
4
5
6
7
8
9
10//在windows平台, 这里的Rep是 整型(不一定是int,可能是int64)
typedef duration<Rep, ratio<3600,1>> hours; //std::ratio<3600>
typedef duration<Rep, ratio<60,1>> minutes; //std::ratio<60>
typedef duration<Rep, ratio<1,1>> seconds; //std::ratio<1>
typedef duration<Rep, ratio<1,1000>> milliseconds;
typedef duration<Rep, ratio<1,1000000>> microseconds;
typedef duration<Rep, ratio<1,1000000000>> nanoseconds;
//当然你也可以完全自己定义, 比如说我定义半分钟
typedef duration<long, ratio<30>> halfminutes;之后你直接使用这些定义好的单位即可(不用再直接使用duration), 例如:
1
2
3//线程睡眠
std::this_thread::sleep_for(std::chrono::seconds(3)); //休眠3秒
std::this_thread::sleep_for(std::chrono::milliseconds (100)); //休眠100毫秒但是注意和
std::
名字空间区分, 例如std::nano
;std::chrono::
下一般是全拼写.
其他一些重要的方法:
count()
1
constexpr rep count() const;//获取时钟(单位)个数, 与单位无关
例如:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
int main(void)
{
std::chrono::milliseconds ms{3}; // 3 milliseconds
// 6000 microseconds constructed from 3 milliseconds
std::chrono::microseconds us = 2*ms;
// 30Hz clock using fractional ticks
std::chrono::duration<double, std::ratio<1, 30>> hz30(3.5);
std::cout << "3 ms duration has " << ms.count() << " ticks\n"
<< "6000 us duration has " << us.count() << " ticks\n"
<< "3.5 30Hz duration has " << hz30.count() << " ticks\n";
}
/* output:
3 ms duration has 3 ticks //count = 3
6000 us duration has 6000 ticks //count =6000
3.5 30Hz duration has 3.5 ticks //cout = 3.5
*/并且注意上面的赋值
us = 2*ms
, 大单位毫秒赋值给微妙是可以的, 但是反过来产生小数就不行了, 这个时候需要自己定义了:1
2
3
4typedef duration<double, ratio<1,1000>> my_millseconds;//毫秒
std::chrono::microseconds us(3);//微妙
my_millseconds ms = 2us; //微妙转换给毫秒, 产生小数也可以用来计算时间间隔: (直接加减)
1
2
3
4
5
6
7std::chrono::minutes t1( 10 );
std::chrono::seconds t2( 60 );
std::chrono::seconds t3 = t1 - t2; //大单位赋值给了小单位, 没有小数产生
std::cout << t3.count() << " second" << std::endl;
std::cout << chrono::duration_cast<chrono::minutes>( t3 ).count()
<<" minutes" << std::endl;duration_cast()
不同单位类型转换, 但是它只会简单的取整, 例如1
2
3
4
5
6seconds s(30); //30秒, 转换成分, 是0.5分, 结果下面却输出0
auto m = duration_cast<minutes>(s); // 0
cout << m << endl; //duration对象可以直接输出
seconds s(301);
cout << duration_cast<minutes>(s)<< endl; //5最好是拿小单位去接收大单位的值, 因为不会有小数部分, 也就不存在被忽略.
- 其他转换
对已经存在的duration对象进行其他单位的转换操作(而不是简单的小数截断)1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22//c++17标准
floor() //取上限
seil() //去下限
round() //四舍五入
//注意使用的时候按照类模板的方式(比Boost中更加丰富的模板参数)
template <class ToDuration, class Rep, class Period>
constexpr ToDuration floor(const duration<Rep, Period>& d);
``
* 最大最小单位数.
也就是某个计时单位的最大最小值. 一般取决于 `Rep`的类型.
```c++
//duration 类的成员函数
static constexpr duration max(); //返回最大的计时单位个数
static constexpr duration min(); //返回最小的计时单位个数
static constepr duration zero(); //返回0个单位的时间间隔
//例如:
//static constexpr duration min(); //duration类的成员方法
//它返回 std::chrono::duration(std::chrono::duration_values<rep>::min())
//而在std::chrono::duration_values类中的成员方法 min()返回
//returns std::numeric_limits<Rep>::lowest()
clock
时钟, duration相当于一个自带单位的时间间隔统计, 那么时钟就是制定计时的仪器; (时间点time_point 记录具体的时间戳, 也是需要时钟的) .这个封装类, 一是要提供计时手段, 另外第一个重要的作用就是用来获取当前时间, 以及实现time_t和time_point的相互转换.
包含三种不同的时钟:
- system_clock 从系统获取的时钟(但是系统时间可以被修改)
- steady_clock 不能被修改的时钟(从开机开始计算)
- high_resolution_clock 高精度时钟(实际上是system_clock或者steady_clock的别名)
steady_clock可以获取稳定可靠的时间间隔, 后一次调用now()的值和前一次的差值是不因为修改了系统时间而改变, 它保证了稳定的时间间隔.
主要方法:
通过 now() 获取当前时间:
1
2
3std::chrono::system_clock::time_point t1 = std::chrono::system_clock::now();
//等价于
std::chrono::time_point<system_clock> t1 = std::chrono::system_clock::now();通过 to_time_t() 可以将一个 time_point 转换为ctime. (记录时长从 duration 变成 long int)
1
2std::time_t now_c = std::chrono::system_clock::to_time_t(time_point);
printf("%s\n", ctime(&now_c));通过from_time_t()来将一个 ctime 转换成 time_point
time_point
具体的时间点, 具体的某个时刻, 相对于epoch的经过的时间单位的统计. time_point表示一个时间点, 用来获取1970.1.1以来的秒数和当前的时间, 可以做一些时间的比较和算术运算, 可以和ctime库结合起来显示时间. time_point 必须要 clock 来计时, time_point 有一个函数 time_from_eproch()
用来获得1970年1月1日到 time_point 时间经过的 duration. 例如:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//计算过了多少天
int main (void)
{
using namespace std::chrono;
typedef duration<int, std::ratio<60*60*24>> days_type; //天
//auto now = system_clock::now();
time_point<system_clock> now( system_clock::now());
time_point<system_clock, days_type> today = time_point_cast<days_type>(now);
std::cout << today.time_since_epoch().count() << " days since epoch" << std::endl;
return 0;
}
比较重要的方法就是:
duration time_since_epoch() const;
案例代码如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
int main(void)
{
using namespace std::chrono;
time_point<system_clock> p2, p3;
p2 = system_clock::now();
p3 = p2 - hours(24); //昨天的此刻
std::cout << "yesterday, hours since epoch: "
<< duration_cast<hours>(
p3.time_since_epoch()).count()
<< std::endl;
std::cout << "hours since epoch: "
<< duration_cast<hours>(
p2.time_since_epoch()).count()
<< std::endl;
return 0;
}time_point_cast
把 time_point 从一种类型的 duration 转换成另一种单位类型的duration .
定义如下:1
2
3
4
5
6
7template <class ToDuration, class Clock, class Duration>
time_point<Clock, ToDuration> time_point_cast(
const time_point<Clock, Duration> &t);
```
其实现也非常简单:
```c++
return time_point<Clock, ToDuration>(duration_cast<ToDuration>(t.time_since_epoch()))使用案例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
using Clock = std::chrono::high_resolution_clock;
using Ms = std::chrono::milliseconds;
using Sec = std::chrono::seconds;
//使用TimePoint, 要指定duration, 例如Ms或者Sec
template<class Duration>
using TimePoint = std::chrono::time_point<Clock, Duration>;
inline void print_ms(const TimePoint<Ms>& time_point)
{
std::cout << time_point.time_since_epoch().count() << " ms\n";
}
int main()
{
TimePoint<Sec> time_point_sec(Sec(4));
// implicit cast, no precision loss
//(大单位向小单位转换)
TimePoint<Ms> time_point_ms(time_point_sec);
print_ms(time_point_ms); // 4000 ms
time_point_ms = TimePoint<Ms>(Ms(5756));
// explicit cast, need when precision loss may happens
// 5756 truncated to 5000
time_point_sec = std::chrono::time_point_cast<Sec>(time_point_ms);
//调用的时候, 大单位向小单位赋值
print_ms(time_point_sec); // 5000 ms
}
综合案例
实现一个简单的 timer 计时器(一般采用steady_clock).1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
using namespace std;
using namespace std::chrono;
//默认以毫秒为单位
class Timer final
{
private:
typedef high_resolution_clock clock_type;
typedef milliseconds duration_type;
typedef long int64_t;
time_point<clock_type> m_begin;
public:
Timer() : m_begin(clock_type::now()) {}
~Timer() = default;
void restart() { m_begin = clock_type::now(); }
//默认输出毫秒
int64_t elapsed() const
{
/*return std::chrono::duration::round<duration_type>
(clock_type::now() - m_begin).count();
//编译器不支持 C++17
*/
return duration_cast<duration_type>(clock_type::now() - m_begin).count();
//直接返回 duration对象 也可以直接cout输出
}
//下面采用重载, 而不是模板参数, 避免用户传参麻烦
//微秒
int64_t elapsed_micro() const
{
return duration_cast<microseconds>(
clock_type::now() - m_begin).count();
}
//秒
int64_t elapsed_seconds() const
{
return duration_cast<seconds>(clock_type::now() - m_begin).count();
}
};
void fun()
{
for(int i = 0; i < 100; i++) {
cout << "hello word" << endl;
}
}
int main()
{
Timer t; //开始计时
fun();
cout<<t.elapsed_micro()<<endl; //打印微秒()
cout<<t.elapsed()<<endl; //打印fun函数耗时多少毫秒
cout<<t.elapsed_seconds()<<endl; //打印秒
return 0;
}
尾巴
C++这套库, 虽然只有三个clock, time_point, duration, 熟悉起来还是要花一段时间的.