一、问题提出
会议中有同学提到使用mktime遇到一些问题: 1) 设置tm_isdst后速度很慢 2) 设置TZ环境变量提速极大 所以想调查下具体情况。
二、测试和检验
环境(不同环境可能结果迥异,以下所述仅对本环境有效)
$ cat /proc/version Linux version --tlinux2-.tl2 ([email protected]) (gcc version (Red Hat -) (GCC) ) # SMP Fri Apr :: CST $ getconf -a | grep glibc -i GNU_LIBC_VERSION glibc 2.17
首先写了个简单的mktime测试。
#include <sys/time.h> #include <stdlib.h> #include <stdio.h> #include <time.h> typedef int64_t timestamp_t; static timestamp_t get_timestamp() { struct timeval tv = {}; gettimeofday(&tv, ); + (timestamp_t)tv.tv_usec; } static void call_mktime(int isdst) { struct tm tm = {}; tm.tm_year = - ; tm.tm_mon = - ; tm.tm_mday = ; tm.tm_hour = ; tm.tm_min = ; tm.tm_sec = ; tm.tm_isdst = isdst; == mktime(&tm)) { abort(); } } int main() { , }; for (const auto &isdst: isdsts) { timestamp_t t1 = get_timestamp(); ; ; i < N; ++i) { call_mktime(isdst); } timestamp_t t2 = get_timestamp(); printf("isdst=%d rounds %d avg cost %4.2f us\n", isdst, N, 1.0*(t2-t1)/N); } ; }
跑一下,得到结果如下:
还真的很慢啊!慢的掉渣了!
But!真的是这样吗?
要不试试其他时区?就用美国东部时间好了。
奇迹发生了,不慢啊,也就几微秒而已,虽然慢了一点点,但绝对没有那么夸张。
三、基本结论
跟着这个思路,稍微扩大一下可变的参数,包括下面几个因子:
- 日历时间
- 时区配置
- 输入isdst
最后跑出一个结果(源代码见后):
|
|
| isdst | ||
日历时间 | 时区配置 | 夏令时 | 1 | 0 | -1 |
1688-06-01 02:00 | Asia/Shanghai | N | 103.95 | 0.23 | 0.23 |
| US/Eastern | N | 114.6 | 0.26 | 0.23 |
| America/Jujuy | N | 125.26 | 0.27 | 0.25 |
1960-06-01 02:00 | Asia/Shanghai | N | 158.9 | 0.3 | 0.29 |
| US/Eastern | Y | 0.6 | 4.3 | 0.39 |
| America/Jujuy | Y | 0.48 | 67.24 | 0.34 |
1986-06-01 02:00 | Asia/Shanghai | Y | 0.48 | 2.47 | 0.3 |
| US/Eastern | Y | 0.44 | 2.73 | 0.3 |
| America/Jujuy | N | 54.67 | 0.34 | 0.32 |
2016-01-01 02:00 | Asia/Shanghai | N | 501.6 | 0.76 | 0.76 |
| US/Eastern | N | 4.49 | 0.32 | 0.31 |
| America/Jujuy | N | 507.45 | 0.81 | 0.78 |
2016-05-01 02:00 | Asia/Shanghai | N | 505.49 | 0.7 | 0.7 |
| US/Eastern | Y | 0.64 | 3.77 | 0.31 |
| America/Jujuy | N | 514.04 | 0.76 | 0.77 |
最后两列是一次mktime调用消耗的微秒数。 注意America/Jujuy这个时区,非常有趣。
从这张表格可以总结出一个基本结论: 当tm_isdst设置不当时,调用mktime会消耗更多的时间。
- 如果当时的日历时间是夏令时,那么
isdst=1
速度比isdst=0
快 ; - 如果当时的日立时间是常规时,那么
isdst=1
速度比isdst=0
慢 ; - 调用mktime,可以传入
isdst=-1
,让glibc根据时区自动决定DST
标记。
至于KM文章中提到的设置TZ环境变量导致的性能差异,是非常小的,有兴趣的可以做个测试。
四、进一步调查
为什么isdst配置不当时,速度会相差这么多?这和mktime的实现有关。
mktime转换年月日格式的时间到时间戳,分几个步骤
- 6次循环,猜测得到一个时间戳t1,调用localtime(t1)能够得到正确的年月日表示。但它的夏令时标记(isdst)可能和用户传递进来的不一致。
- 如果夏令时标记和用户调用传递的isdst不同(0 vs 1, 1 vs 0),以t1为基准,前后搜寻合适的日历时,如果找到一个日历时,它的DST标记符合,以该日历时所处的DST为准。如果无法搜寻到合适的结果,直接返回t1
- 搜寻思想:以当前时间为中心,前后搜寻,找到一个与输入参数匹配的夏令时/冬令时区间,以该区间的配置来校准t1。
- 搜寻算法:
- 步长:601200秒,这是所有夏令时区间中最短的一个周期:7天,时区为:America/Recife
- 范围:以t1为中心的536454000秒区间。这是所有夏令时区间中最长的一个周期:17年,时区为America/Jujuy。范围:[t-536454000/2+601200,t+536454000/2+601200],最大故迭代次数894次。
- 迭代: 对当前时间戳tx调用localtime(tx),若结果的isdst和输入的isdst相同,命中,跳出循环。否则继续。
- 命中:根据命中时的DST设置,找到正确的时间戳t3,转换成功。
迭代次数越多,耗时就越多。如果步骤1转换的posix time距离最近的匹配区间(夏令时/冬令时)很远,搜寻耗时就很长。
Asio/Shanghai的夏令时从1991年废除,而US/Eastern每年都有夏令时,所以,大部分情况下前者的迭代次数远大于后者,这也能很好的解释上面的图表。
用zdump
看一下不同时区数据库的信息,注意1688
和1986
两个测试日历时间的选取。
五、附录
1. test_timezone.cpp
测试不同时区的mktime性能
#include <sys/time.h> #include <stdlib.h> #include <stdio.h> #include <time.h> typedef int64_t timestamp_t; static timestamp_t get_timestamp() { struct timeval tv = {}; gettimeofday(&tv, ); + (timestamp_t)tv.tv_usec; } struct calendar_time { int year; int month; int day; int hour; int minute; int second; }; static void call_mktime( const calendar_time &calendar, int isdst) { struct tm tm = {}; tm.tm_year = calendar.year - ; tm.tm_mon = calendar.month- ; tm.tm_mday = calendar.day; tm.tm_hour = calendar.hour; tm.tm_min = calendar.minute; tm.tm_sec = calendar.second; tm.tm_isdst = isdst; == mktime(&tm)) { abort(); } } int main() { const char *timeonzes[] = { "Asia/Shanghai", "US/Eastern", "America/Jujuy"}; calendar_time times[] = { {, , , , , }, {, , , , , }, {, , , , , }, {, , , , , }, {, , , , , }, {, , , , , }, {, , , , , }, }; , , -}; for (const auto &calendar: times) { ]; for (const auto &tz: timeonzes) { for (const auto &isdst: isdsts) { setenv(); timestamp_t t1 = get_timestamp(); ; ; i < N; ++i) { call_mktime(calendar, isdst); } timestamp_t t2 = get_timestamp(); printf("calendar: %04d-%02d-%02d %02d:%02d:%02d ", calendar.year, calendar.month, calendar.day, calendar.hour, calendar.minute, calendar.second); printf("%-20s isdst=%2d rounds %d avg cost %4.2f us\n", tz, isdst, N, 1.0*(t2-t1)/N); } printf("\n"); } printf("-------------------------------------------\n"); } ; }
2. test_setenv.cpp
测试setenv("TZ")和不设置时的性能差别。
#include <sys/time.h> #include <stdlib.h> #include <stdio.h> #include <time.h> typedef int64_t timestamp_t; static timestamp_t get_timestamp() { struct timeval tv = {}; gettimeofday(&tv, ); + (timestamp_t)tv.tv_usec; } struct calendar_time { int year; int month; int day; int hour; int minute; int second; }; static void call_mktime( const calendar_time &calendar, int isdst) { struct tm tm = {}; tm.tm_year = calendar.year - ; tm.tm_mon = calendar.month- ; tm.tm_mday = calendar.day; tm.tm_hour = calendar.hour; tm.tm_min = calendar.minute; tm.tm_sec = calendar.second; tm.tm_isdst = isdst; == mktime(&tm)) { abort(); } } int main() { const char *tz = "Asia/Shanghai"; calendar_time times[] = { {, , , , , }, {, , , , , }, {, , , , , }, {, , , , , }, }; for (const auto &calendar: times) { printf("calendar: %04d-%02d-%02d %02d:%02d:%02d\n", calendar.year, calendar.month, calendar.day, calendar.hour, calendar.minute, calendar.second); { unsetenv("TZ"); timestamp_t t1 = get_timestamp(); ; ; i < N; ++i) { call_mktime(calendar, ); } timestamp_t t2 = get_timestamp(); printf("unsetenv %-20s rounds %d avg cost %4ld us\n", tz, N, (t2-t1)/N); } { setenv(); timestamp_t t1 = get_timestamp(); ; ; i < N; ++i) { call_mktime(calendar, ); } timestamp_t t2 = get_timestamp(); printf("setenv %-20s rounds %d avg cost %4ld us\n", tz, N, (t2-t1)/N); } printf("\n"); } ; }