Linux下用户态mktime实现参考

1、背景

继上周《Linux下mktime耗时测试》,本次通过参考glibc、内核源码进行实验

2、接口说明

时间转换接口,主要用于所指向的结构转换为自 1970 年1月1日以来持续时间的秒数,发生错误时返回-1。

SYNOPSIS         top
       #include <time.h>

       time_t mktime(struct tm *timeptr);
DESCRIPTION
       The functionality described on this reference page is aligned
       with the ISO C standard. Any conflict between the requirements
       described here and the ISO C standard is unintentional. This
       volume of POSIX.12017 defers to the ISO C standard.

       The mktime() function shall convert the broken-down time,
       expressed as local time, in the structure pointed to by timeptr,
       into a time since the Epoch value with the same encoding as that
       of the values returned by time().  The original values of the
       tm_wday and tm_yday components of the structure shall be ignored,
       and the original values of the other components shall not be
       restricted to the ranges described in <time.h>.
---
       Broken-down time is stored in the structure tm, which is defined in <time.h> as follows:

           struct tm {
               int tm_sec;    /* Seconds (0-60) */
               int tm_min;    /* Minutes (0-59) */
               int tm_hour;   /* Hours (0-23) */
               int tm_mday;   /* Day of the month (1-31) */
               int tm_mon;    /* Month (0-11) */
               int tm_year;   /* Year - 1900 */
               int tm_wday;   /* Day of the week (0-6, Sunday = 0) */
               int tm_yday;   /* Day in the year (0-365, 1 Jan = 0) */
               int tm_isdst;  /* Daylight saving time */
           };

3、实现

3.1 参考内核实现(最简单)

核心接口实现,几行代码就搞定


/* Converts Gregorian date to seconds since 1970-01-01 00:00:00.
 * Assumes input in normal date format, i.e. 1980-12-31 23:59:59
 * => year=1980, mon=12, day=31, hour=23, min=59, sec=59.
 *
 * [For the Julian calendar (which was used in Russia before 1917,
 * Britain & colonies before 1752, anywhere else before 1582,
 * and is still in use by some communities) leave out the
 * -year/100+year/400 terms, and add 10.]
 *
 * This algorithm was first published by Gauss (I think).
 *
 * WARNING: this function will overflow on 2106-02-07 06:28:16 on
 * machines where long is 32-bit! (However, as time_t is signed, we
 * will already get problems at other places on 2038-01-19 03:14:08)
 */
unsigned long __mktime_kernel(const unsigned int year0, const unsigned int mon0,
                              const unsigned int day,   const unsigned int hour,
                              const unsigned int min,   const unsigned int sec)
{
    unsigned int mon = mon0, year = year0;

    /* 1..12 -> 11,12,1..10 */
    if (0 >= (int)(mon -= 2)) {
        mon += 12;  /* Puts Feb last since it has leap day */
        year -= 1;
    }

    return ((((unsigned long)
              (year / 4 - year / 100 + year / 400 + 367 * mon / 12 + day) +
              year * 365 - 719499
             ) * 24 + hour /* now have hours */
            ) * 60 + min /* now have minutes */
           ) * 60 + sec; /* finally seconds */
}

然后再仔细看他调用的地方,发现时间如果是太小了得有个+100的修正;

time_t sdk_mktime_kernel(struct tm *tp)
{
    if (!tp) {
        printf("NULL\n");
        return -1;
    }

    if (unlikely(tp->tm_year + 1900 < 1970)) {
        tp->tm_year += 100;
    }
    return __mktime_kernel(tp->tm_year + 1900, tp->tm_mon + 1, tp->tm_mday,
                           tp->tm_hour, tp->tm_min, tp->tm_sec);
}

3.2 glibc实现参考(裁剪)

核心实现是 __mktime_internal,然后这里面处理比较复杂,就是拆解了一些:
1、去掉了一个夏令时DST相关的逻辑;
2、去掉了一个闰秒相关的逻辑;

#define EPOCH_YEAR 1970
#define TM_YEAR_BASE 1900

#if INT_MAX <= (LONG_MAX / 4 / 366 / 24 / 60 / 60)
typedef long int long_int;
#else
typedef long long int long_int;
#endif

static const unsigned short int __g_mon_yday[2][13] = {
    /* Normal years.  */
    { 0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334, 365 },
    /* Leap years.  */
    { 0, 31, 60, 91, 121, 152, 182, 213, 244, 274, 305, 335, 366 }
};

#ifdef __cplusplus
extern "C" {
#endif

/* Is YEAR + TM_YEAR_BASE a leap year?  */
static inline bool __is_leapyear(long_int year)
{
    /* Don't add YEAR to TM_YEAR_BASE, as that might overflow.
       Also, work even if YEAR is negative.  */
    return ((year & 3) == 0 &&
            (year % 100 != 0 || ((year / 100) & 3) == (- (TM_YEAR_BASE / 100) & 3)));
}

/* Shift A right by B bits portably, by dividing A by 2**B and
   truncating towards minus infinity.  B should be in the range 0 <= B
   <= long_BITS - 2, where long_BITS is the number of useful
   bits in a long.  long_BITS is at least 32.
   ISO C99 says that A >> B is implementation-defined if A < 0.  Some
   implementations (e.g., UNICOS 9.0 on a Cray Y-MP EL) don't shift
   right in the usual way when A < 0, so SHR falls back on division if
   ordinary A >> B doesn't seem to be the usual signed shift.  */
static inline long_int __shr(long_int a, int b)
{
    long_int one = 1;
    return ((-one >> 1 == -1) ? (a >> b) : (a + (a < 0)) / (one << b) - (a < 0));
}

static inline long_int __do_ydhms_diff(long_int year1, long_int yday1, int hour1, int min1,
                                       int sec1,
                                       long_int year0, long_int yday0, int hour0, int min0, int sec0)
{
//    verify(-1 / 2 == 0);

    /* Compute intervening leap days correctly even if year is negative.
       Take care to avoid integer overflow here.  */
    int a4 = __shr(year1, 2) + __shr(TM_YEAR_BASE, 2) - !(year1 & 3);
    int b4 = __shr(year0, 2) + __shr(TM_YEAR_BASE, 2) - !(year0 & 3);
    int a100 = (a4 + (a4 < 0)) / 25 - (a4 < 0);
    int b100 = (b4 + (b4 < 0)) / 25 - (b4 < 0);
    int a400 = __shr(a100, 2);
    int b400 = __shr(b100, 2);
    int intervening_leap_days = (a4 - b4) - (a100 - b100) + (a400 - b400);

    /* Compute the desired time without overflowing.  */
    long_int years = year1 - year0;
    long_int days = 365 * years + yday1 - yday0 + intervening_leap_days;
    long_int hours = 24 * days + hour1 - hour0;
    long_int minutes = 60 * hours + min1 - min0;
    long_int seconds = 60 * minutes + sec1 - sec0;
    return seconds;
}

/* Convert *TP to a __time64_t value, inverting
   the monotonic and mostly-unit-linear conversion function CONVERT.
   Use *OFFSET to keep track of a guess at the offset of the result,
   compared to what the result would be for UTC without leap seconds.
   If *OFFSET's guess is correct, only one CONVERT call is needed.
   If successful, set *TP to the canonicalized struct tm;
   otherwise leave *TP alone, return ((time_t) -1) and set errno.
   This function is external because it is used also by timegm.c. */
static inline time_t __mktime_internal(struct tm *tp)
{
    /* The maximum number of probes (calls to CONVERT) should be enough
         to handle any combinations of time zone rule changes, solar time,
         leap seconds, and oscillations around a spring-forward gap.
         POSIX.1 prohibits leap seconds, but some hosts have them anyway.  */
//    int remaining_probes = 6;

    /* Time requested.  Copy it in case CONVERT modifies *TP; this can
       occur if TP is localtime's returned value and CONVERT is localtime.  */
    int sec  = tp->tm_sec;
    int min  = tp->tm_min;
    int hour = tp->tm_hour;
    int mday = tp->tm_mday;
    int mon  = tp->tm_mon;
    int year_requested = tp->tm_year;

    /* Ensure that mon is in range, and set year accordingly.  */
    int mon_remainder = mon % 12;
    int negative_mon_remainder = mon_remainder < 0;
    int mon_years = mon / 12 - negative_mon_remainder;
    long_int lyear_requested = year_requested;
    long_int year = lyear_requested + mon_years;

    /* The other values need not be in range:
       the remaining code handles overflows correctly.  */

    /* Calculate day of year from year, month, and day of month.
       The result need not be in range.  */
    int mon_yday = ((__g_mon_yday[__is_leapyear(year)]
                     [mon_remainder + 12 * negative_mon_remainder]) - 1);
    long_int lmday = mday;
    long_int yday  = mon_yday + lmday;

    if (tp->tm_isdst != 0) {
        printf("WARN: DST not support !!!\n");
    }

    /* skip LEAP_SECONDS_POSSIBLE &
     * skip NEGATIVE_OFFSET_GUESS
     * skip tm.tm_isdst */
    return (time_t)__do_ydhms_diff(year, yday, hour, min, sec,
                                   EPOCH_YEAR - TM_YEAR_BASE, 0, 0, 0, 0);
}

最后封装统一了一下格式

time_t sdk_mktime_glibc(struct tm *tp)
{
    if (!tp) {
        printf("NULL\n");
        return -1;
    }
    return __mktime_internal(tp);
}

3.3 实验结果

2021-09-26 20:24:09
Running ./bm_mktime
Run on (16 X 2199.09 MHz CPU s)
CPU Caches:
  L1 Data 32K (x4)
  L1 Instruction 32K (x4)
  L2 Unified 256K (x4)
  L3 Unified 25600K (x4)
Load Average: 0.00, 0.00, 0.00
----------------------------------------------------------------------------------
Benchmark                                        Time             CPU   Iterations
----------------------------------------------------------------------------------
bm_mktime/mktime_TZ_NULL_isDST                 886 ns          886 ns       787062
bm_mktime/mktime_TZ_NULL_noDST                 886 ns          886 ns       789607
bm_mktime/mktime_TZ_empty_isDST                406 ns          406 ns      1723817
bm_mktime/mktime_TZ_empty_noDST                406 ns          406 ns      1727658
bm_mktime/mktime_TZ_shanghai_isDST             405 ns          405 ns      1721386
bm_mktime/mktime_TZ_shanghai_noDST             406 ns          406 ns      1725817
bm_mktime/sdk_mktime_glibc                    18.4 ns         18.4 ns     38107389
bm_mktime/sdk_mktime_kernel                   18.4 ns         18.4 ns     38096369

4、结论

实验结果看,用户态直接计算,提升呈几何增长;glibc、kernel实现测试的速度不相上下;但实现复杂度来看,kernel版本更加简洁。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
static __time64_t __cdecl _make__time64_t ( struct tm *tb, int ultflag ) { __time64_t tmptm1, tmptm2, tmptm3; struct tm tbtemp; long dstbias = 0; long timezone = 0; _VALIDATE_RETURN( ( tb != NULL ), EINVAL, ( ( __time64_t )( -1 ) ) ) /* * First, make sure tm_year is reasonably close to being in range. */ if ( ((tmptm1 = tb->tm_year) < _BASE_YEAR - 1) || (tmptm1 > _MAX_YEAR64 + 1) ) goto err_mktime; /* * Adjust month value so it is in the range 0 - 11. This is because * we don't know how many days are in months 12, 13, 14, etc. */ if ( (tb->tm_mon < 0) || (tb->tm_mon > 11) ) { tmptm1 += (tb->tm_mon / 12); if ( (tb->tm_mon %= 12) < 0 ) { tb->tm_mon += 12; tmptm1--; } /* * Make sure year count is still in range. */ if ( (tmptm1 < _BASE_YEAR - 1) || (tmptm1 > _MAX_YEAR64 + 1) ) goto err_mktime; } /***** HERE: tmptm1 holds number of elapsed years *****/ /* * Calculate days elapsed minus one, in the given year, to the given * month. Check for leap year and adjust if necessary. */ tmptm2 = _days[tb->tm_mon]; if ( _IS_LEAP_YEAR(tmptm1) && (tb->tm_mon > 1) ) tmptm2++; /* * Calculate elapsed days since base date (midnight, 1/1/70, UTC) * * * 365 days for each elapsed year since 1970, plus one more day for * each elapsed leap year. no danger of overflow because of the range * check (above) on tmptm1. */ tmptm3 = (tmptm1 - _BASE_YEAR) * 365 + _ELAPSED_LEAP_YEARS(tmptm1); /* * elapsed days to current month (still no possible overflow) */ tmptm3 += tmptm2; /* * elapsed days to current date. */ tmptm1 = tmptm3 + (tmptm2 = (__time64_t)(tb->tm_mday)); /***** HERE: tmptm1 holds number of elapsed days *****/ /* * Calculate elapsed hours since base date */ tmptm2 = tmptm1 * 24; tmptm1 = tmptm2 + (tmptm3 = (__time64_t)tb->tm_hour); /***** HERE: tmptm1 holds number of elapsed hours *****/ /* * Calculate elapsed minutes since base date */ tmptm2 = tmptm1 * 60; tmptm1 = tmptm2 + (tmptm3 = (__time64_t)tb->tm_min); /***** HERE: tmptm1 holds number of elapsed minutes *****/ /* * Calculate elapsed seconds since base date */ tmptm2 = tmptm1 * 60; tmptm1 = tmptm2 + (tmptm3 = (__time64_t)tb->tm_sec); /***** HERE: tmptm1 holds number of elapsed seconds *****/ if ( ultflag ) { /* * Adjust for timezone. No need to check for overflow since * localtime() will check its arg value */ __tzset(); _ERRCHECK(_get_dstbias(&dstbias;)); _ERRCHECK(_get_timezone(&timezone;)); tmptm1 += timezone; /* * Convert this second count back into a time block structure. * If localtime returns NULL, return an error. */ if ( _localtime64_s(&tbtemp;, &tmptm1;) != 0 ) goto err_mktime; /* * Now must compensate for DST. The ANSI rules are to use the * passed-in tm_isdst flag if it is non-negative. Otherwise, * compute if DST applies. Recall that tbtemp has the time without * DST compensation, but has set tm_isdst correctly. */ if ( (tb->tm_isdst > 0) || ((tb->tm_isdst < 0) && (tbtemp.tm_isdst > 0)) ) { tmptm1 += dstbias; if ( _localtime64_s(&tbtemp;, &tmptm1;) != 0 ) goto err_mktime; } } else { if ( _gmtime64_s(&tbtemp;, &tmptm1;) != 0) goto err_mktime; } /***** HERE: tmptm1 holds number of elapsed seconds, adjusted *****/ /***** for local time if requested *****/ *tb = tbtemp; return tmptm1; err_mktime: /* * All errors come to here */ errno = EINVAL; return (__time64_t)(-1); }

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值