一、问题提出

会议中有同学提到使用mktime遇到一些问题: 1) 设置tm_isdst后速度很慢 2) 设置TZ环境变量提速极大 所以想调查下具体情况。

 

二、测试和检验

环境(不同环境可能结果迥异,以下所述仅对本环境有效)

$ cat /proc/version
Linux version --tlinux2-.tl2 (mockbuild@TENCENT64.site)
(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!真的是这样吗? 
 
要不试试其他时区?就用美国东部时间好了。

奇迹发生了,不慢啊,也就几微秒而已,虽然慢了一点点,但绝对没有那么夸张。

三、基本结论

跟着这个思路,稍微扩大一下可变的参数,包括下面几个因子:

  1. 日历时间
  2. 时区配置
  3. 输入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转换年月日格式的时间到时间戳,分几个步骤

  1. 6次循环,猜测得到一个时间戳t1,调用localtime(t1)能够得到正确的年月日表示。但它的夏令时标记(isdst)可能和用户传递进来的不一致。
  2. 如果夏令时标记和用户调用传递的isdst不同(0 vs 1, 1 vs 0),以t1为基准,前后搜寻合适的日历时,如果找到一个日历时,它的DST标记符合,以该日历时所处的DST为准。如果无法搜寻到合适的结果,直接返回t1
  3. 搜寻思想:以当前时间为中心,前后搜寻,找到一个与输入参数匹配的夏令时/冬令时区间,以该区间的配置来校准t1。
  4. 搜寻算法: 
    • 步长: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看一下不同时区数据库的信息,注意16881986两个测试日历时间的选取。

五、附录

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");
    }

    ;
}

六、参考文档

  1. https://github.com/lattera/glibc/blob/master/time/mktime.c
04-26 15:23