mktime函数性能分析

博客搬家,原地址:https://langzi989.github.io/2019/01/02/mktime性能分析/

mktime是一个将break-down时间(struct tm)转化为日历时间(time_t)的转换函数。它的转换与struct tm中的 tm_wday、tm_yday无关,当进行转换时,mktime会通过struct tm的其他成员重新矫正该值。若struct tm中的成员是非法的的,mktime将会自动校正,如2018-12-32 00:00:00,矫正后为2019-01-01 00:00:00。若给定struct tm不能转换为日历时间,则mktime返回-1。----man mktime

1. 背景

背景:最近工作中遇到一个奇怪的问题,在将原先在32位机器上编译的程序放在64位机器上重新编译之后,然后放到IDC机器运行,发现性能降了100倍左右。在经过性能分析和查阅相关资料后发现,是由于mktime使用不当导致。

#include <iostream>
#include <string>
#include <sys/time.h>
#include <unistd.h>

using namespace std;

#define PrintTime(id)  struct timeval __now1105##id;\
                       gettimeofday(&__now1105##id, 0);

#define PrintTimeDone(id) struct timeval __now21105##id; \
                           gettimeofday(&__now21105##id, 0); \
							printf("timer_%s spend time:%d us\n",#id,(__now21105##id.tv_sec-__now1105##id.tv_sec)* 1000000 + (__now21105##id.tv_usec-__now1105##id.tv_usec));

static void get_time(const std::string& time_str)
{
	struct tm temp_tm;
	strptime(time_str.c_str(), "%Y-%m-%d %H:%M:%S", &temp_tm);
	PrintTime(mktime);
	time_t temp = mktime(&temp_tm);
	PrintTimeDone(mktime);
}

int main()
{
	for (int i = 0; i < 10; i++)
    	get_time("2018-12-27 00:00:00");
	return 0;
}

32位机器(i686)编译运行结果:

timer_mktime spend time:51 us
timer_mktime spend time:4 us
timer_mktime spend time:2 us
timer_mktime spend time:3 us
timer_mktime spend time:3 us
timer_mktime spend time:3 us
timer_mktime spend time:3 us
timer_mktime spend time:2 us
timer_mktime spend time:2 us
timer_mktime spend time:2 us

64位机器(x86_64 )编译运行结果:

timer_mktime spend time:181 us
timer_mktime spend time:156 us
timer_mktime spend time:138 us
timer_mktime spend time:138 us
timer_mktime spend time:137 us
timer_mktime spend time:145 us
timer_mktime spend time:143 us
timer_mktime spend time:138 us
timer_mktime spend time:138 us
timer_mktime spend time:145 us

造成上述问题的原因究竟是什么呢?

2. 源码分析mktime性能

2.1 mktime源码:

mktime.c

/* Convert *TP to a time_t value.  */
time_t
mktime (struct tm *tp)
{
#ifdef _LIBC
  /* POSIX.1 8.1.1 requires that whenever mktime() is called, the
     time zone names contained in the external variable 'tzname' shall
     be set as if the tzset() function had been called.  */
  __tzset ();
#endif

  return __mktime_internal (tp, __localtime_r, &localtime_offset);
}

/* Convert *TP to a time_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.
   This function is external because it is used also by timegm.c.  */
time_t
__mktime_internal (struct tm *tp,
		   struct tm *(*convert) (const time_t *, struct tm *),
		   time_t *offset)
{
  time_t t, gt, t0, t1, t2;
  struct tm tm;

  /* 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;
  int isdst = tp->tm_isdst;

  /* 1 if the previous probe was DST.  */
  int dst2;

  /* 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 minor overflows correctly,
     assuming int and time_t arithmetic wraps around.
     Major overflows are caught at the end.  */

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

  time_t guessed_offset = *offset;

  int sec_requested = sec;

  if (LEAP_SECONDS_POSSIBLE)
    {
      /* Handle out-of-range seconds specially,
	 since ydhms_tm_diff assumes every minute has 60 seconds.  */
      if (sec < 0)
	sec = 0;
      if (59 < sec)
	sec = 59;
    }

  /* Invert CONVERT by probing.  First assume the same offset as last
     time.  */

  t0 = ydhms_diff (year, yday, hour, min, sec,
		   EPOCH_YEAR - TM_YEAR_BASE, 0, 0, 0, - guessed_offset);

  if (TIME_T_MAX / INT_MAX / 366 / 24 / 60 / 60 < 3)
    {
      /* time_t isn't large enough to rule out overflows, so check
	 for major overflows.  A gross check suffices, since if t0
	 has overflowed, it is off by a multiple of TIME_T_MAX -
	 TIME_T_MIN + 1.  So ignore any component of the difference
	 that is bounded by a small value.  */

      /* Approximate log base 2 of the number of time units per
	 biennium.  A biennium is 2 years; use this unit instead of
	 years to avoid integer overflow.  For example, 2 average
	 Gregorian years are 2 * 365.2425 * 24 * 60 * 60 seconds,
	 which is 63113904 seconds, and rint (log2 (63113904)) is
	 26.  */
      int ALOG2_SECONDS_PER_BIENNIUM = 26;
      int ALOG2_MINUTES_PER_BIENNIUM = 20;
      int ALOG2_HOURS_PER_BIENNIUM = 14;
      int ALOG2_DAYS_PER_BIENNIUM = 10;
      int LOG2_YEARS_PER_BIENNIUM = 1;

      int approx_requested_biennia =
	(SHR (year_requested, LOG2_YEARS_PER_BIENNIUM)
	 - SHR (EPOCH_YEAR - TM_YEAR_BASE, LOG2_YEARS_PER_BIENNIUM)
	 + SHR (mday, ALOG2_DAYS_PER_BIENNIUM)
	 + SHR (hour, ALOG2_HOURS_PER_BIENNIUM)
	 + SHR (min, ALOG2_MINUTES_PER_BIENNIUM)
	 + (LEAP_SECONDS_POSSIBLE
	    ? 0
	    : SHR (sec, ALOG2_SECONDS_PER_BIENNIUM)));

      int approx_biennia = SHR (t0, ALOG2_SECONDS_PER_BIENNIUM);
      int diff = approx_biennia - approx_requested_biennia;
      int approx_abs_diff = diff < 0 ? -1 - diff : diff;

      /* IRIX 4.0.5 cc miscalculates TIME_T_MIN / 3: it erroneously
	 gives a positive value of 715827882.  Setting a variable
	 first then doing math on it seems to work.
	 ([email protected]) */
      time_t time_t_max = TIME_T_MAX;
      time_t time_t_min = TIME_T_MIN;
      time_t overflow_threshold =
	(time_t_max / 3 - time_t_min / 3) >> ALOG2_SECONDS_PER_BIENNIUM;

      if (overflow_threshold < approx_abs_diff)
	{
	  /* Overflow occurred.  Try repairing it; this might work if
	     the time zone offset is enough to undo the overflow.  */
	  time_t repaired_t0 = -1 - t0;
	  approx_biennia = SHR (repaired_t0, ALOG2_SECONDS_PER_BIENNIUM);
	  diff = approx_biennia - approx_requested_biennia;
	  approx_abs_diff = diff < 0 ? -1 - diff : diff;
	  if (overflow_threshold < approx_abs_diff)
	    return -1;
	  guessed_offset += repaired_t0 - t0;
	  t0 = repaired_t0;
	}
    }

  /* Repeatedly use the error to improve the guess.  */

  for (t = t1 = t2 = t0, dst2 = 0;
       (gt = guess_time_tm (year, yday, hour, min, sec, &t,
			    ranged_convert (convert, &t, &tm)),
	t != gt);
       t1 = t2, t2 = t, t = gt, dst2 = tm.tm_isdst != 0)
    if (t == t1 && t != t2
	&& (tm.tm_isdst < 0
	    || (isdst < 0
		? dst2 <= (tm.tm_isdst != 0)
		: (isdst != 0) != (tm.tm_isdst != 0))))
      /* We can't possibly find a match, as we are oscillating
	 between two values.  The requested time probably falls
	 within a spring-forward gap of size GT - T.  Follow the common
	 practice in this case, which is to return a time that is GT - T
	 away from the requested time, preferring a time whose
	 tm_isdst differs from the requested value.  (If no tm_isdst
	 was requested and only one of the two values has a nonzero
	 tm_isdst, prefer that value.)  In practice, this is more
	 useful than returning -1.  */
      goto offset_found;
    else if (--remaining_probes == 0)
      return -1;

  /* We have a match.  Check whether tm.tm_isdst has the requested
     value, if any.  */
  if (isdst_differ (isdst, tm.tm_isdst))
    {
      /* tm.tm_isdst has the wrong value.  Look for a neighboring
	 time with the right value, and use its UTC offset.
	 Heuristic: probe the adjacent timestamps in both directions,
	 looking for the desired isdst.  This should work for all real
	 time zone histories in the tz database.  */

      /* Distance between probes when looking for a DST boundary.  In
	 tzdata2003a, the shortest period of DST is 601200 seconds
	 (e.g., America/Recife starting 2000-10-08 01:00), and the
	 shortest period of non-DST surrounded by DST is 694800
	 seconds (Africa/Tunis starting 1943-04-17 01:00).  Use the
	 minimum of these two values, so we don't miss these short
	 periods when probing.  */
      int stride = 601200;

      /* The longest period of DST in tzdata2003a is 536454000 seconds
	 (e.g., America/Jujuy starting 1946-10-01 01:00).  The longest
	 period of non-DST is much longer, but it makes no real sense
	 to search for more than a year of non-DST, so use the DST
	 max.  */
      int duration_max = 536454000;

      /* Search in both directions, so the maximum distance is half
	 the duration; add the stride to avoid off-by-1 problems.  */
      int delta_bound = duration_max / 2 + stride;

      int delta, direction;

      for (delta = stride; delta < delta_bound; delta += stride)
	for (direction = -1; direction <= 1; direction += 2)
	  if (time_t_int_add_ok (t, delta * direction))
	    {
	      time_t ot = t + delta * direction;
	      struct tm otm;
	      ranged_convert (convert, &ot, &otm);
	      if (! isdst_differ (isdst, otm.tm_isdst))
		{
		  /* We found the desired tm_isdst.
		     Extrapolate back to the desired time.  */
		  t = guess_time_tm (year, yday, hour, min, sec, &ot, &otm);
		  ranged_convert (convert, &t, &tm);
		  goto offset_found;
		}
	    }
    }

 offset_found:
  *offset = guessed_offset + t - t0;

  if (LEAP_SECONDS_POSSIBLE && sec_requested != tm.tm_sec)
    {
      /* Adjust time to reflect the tm_sec requested, not the normalized value.
	 Also, repair any damage from a false match due to a leap second.  */
      int sec_adjustment = (sec == 0 && tm.tm_sec == 60) - sec;
      if (! time_t_int_add_ok (t, sec_requested))
	return -1;
      t1 = t + sec_requested;
      if (! time_t_int_add_ok (t1, sec_adjustment))
	return -1;
      t2 = t1 + sec_adjustment;
      if (! convert (&t2, &tm))
	return -1;
      t = t2;
    }

  *tp = tm;
  return t;
}

tzset.c

void
__tzset (void)
{
  __libc_lock_lock (tzset_lock);

  tzset_internal (1, 1);

  if (!__use_tzfile)
    {
      /* Set `tzname'.  */
      __tzname[0] = (char *) tz_rules[0].name;
      __tzname[1] = (char *) tz_rules[1].name;
    }

  __libc_lock_unlock (tzset_lock);
}
weak_alias (__tzset, tzset)

/* Interpret the TZ envariable.  */
static void
internal_function
tzset_internal (int always, int explicit)
{
  static int is_initialized;
  const char *tz;

  if (is_initialized && !always)
    return;
  is_initialized = 1;

  /* Examine the TZ environment variable.  */
  tz = getenv ("TZ");
  if (tz == NULL && !explicit)
    /* Use the site-wide default.  This is a file name which means we
       would not see changes to the file if we compare only the file
       name for change.  We want to notice file changes if tzset() has
       been called explicitly.  Leave TZ as NULL in this case.  */
    tz = TZDEFAULT;
  if (tz && *tz == '\0')
    /* User specified the empty string; use UTC explicitly.  */
    tz = "Universal";

  /* A leading colon means "implementation defined syntax".
     We ignore the colon and always use the same algorithm:
     try a data file, and if none exists parse the 1003.1 syntax.  */
  if (tz && *tz == ':')
    ++tz;

  /* Check whether the value changed since the last run.  */
  if (old_tz != NULL && tz != NULL && strcmp (tz, old_tz) == 0)
    /* No change, simply return.  */
    return;

  if (tz == NULL)
    /* No user specification; use the site-wide default.  */
    tz = TZDEFAULT;

  tz_rules[0].name = NULL;
  tz_rules[1].name = NULL;

  /* Save the value of `tz'.  */
  free (old_tz);
  old_tz = tz ? __strdup (tz) : NULL;

  /* Try to read a data file.  */
  __tzfile_read (tz, 0, NULL);
  if (__use_tzfile)
    return;

  /* No data file found.  Default to UTC if nothing specified.  */

  if (tz == NULL || *tz == '\0'
      || (TZDEFAULT != NULL && strcmp (tz, TZDEFAULT) == 0))
    {
      memset (tz_rules, '\0', sizeof tz_rules);
      tz_rules[0].name = tz_rules[1].name = "UTC";
      if (J0 != 0)
	tz_rules[0].type = tz_rules[1].type = J0;
      tz_rules[0].change = tz_rules[1].change = (time_t) -1;
      update_vars ();
      return;
    }

  __tzset_parse_tz (tz);
}

2.2 源码分析结论

从mktime的源码实现中可以看出,mktime的大致执行流程如下:

  • 首先通过调用**__tzset()**对时区进行设置
    • 若TZ环境变量为NULL,尝试调用**__tzfile_read**读取文件中的时区信息
    • 只需要首次设置,若改变或为NULL重新设置
  • 然后调用**__mktime_internal**进行时间转换
    • 检查系统的tm_isdst(是否为夏令时)和传入的struct tm的tm_isdst,若两个不一致(isdst_differ),则进行矫正tm_isdst

从上源码可以得出结论:影响mktime性能的两个因素主要包括两方面,一是TZ设置是否为NULL,二是传入的struct tm参数的tm_isdst成员与系统的是否一致。

3. 实验分析mktime性能

3.1 TZ对mktime的性能影响

3.1.1 实验程序

void test_TZ_effect(int count)
{
	struct tm temp_tm;
	temp_tm.tm_isdst = 0;
	std::string time_str = "2019-01-02 00:00:00";
	strptime(time_str.c_str(), "%Y-%m-%d %H:%M:%S", &temp_tm);

	setenv("TZ", "Asia/Shanghai", 0);
	PrintTime(TZ_SET);
	for (int i = 0; i < count; i++)
	{
		mktime(&temp_tm);
	}
	PrintTimeDone(TZ_SET);

	unsetenv("TZ");
	PrintTime(TZ_UNSET);
	for (int i = 0; i < count; i++)
	{
		mktime(&temp_tm);
	}
	PrintTimeDone(TZ_UNSET);
}

3.1.2 实验结果:

./a.out 10000
32位机器实验结果:
timer_TZ_SET spend time:3945 us
timer_TZ_UNSET spend time:14332 us

64位机器实验结果:
timer_TZ_SET spend time:2566 us
timer_TZ_UNSET spend time:17459 us

3.1.3 实验结果分析

从源码分析可以知道,当TZ为NULL时,将会尝试读取文件,故不设置TZ时性能较低,约为设置的1/6

3.2 tm_isdst对mktime性能影响

3.2.1 实验程序

void test_TZ_effect(int count)
{
	struct tm temp_tm;
	std::string time_str = "2019-01-02 00:00:00";
	strptime(time_str.c_str(), "%Y-%m-%d %H:%M:%S", &temp_tm);

	setenv("TZ", "Asia/Shanghai", 0);
	PrintTime(DST_INNER_0);
	for (int i = 0; i < count; i++)
	{
		temp_tm.tm_isdst = 0;
		mktime(&temp_tm);
	}
	PrintTimeDone(DST_INNER_0);

	PrintTime(DST_INNER_1);
	for (int i = 0; i < count; i++)
	{
		temp_tm.tm_isdst = 1;
		mktime(&temp_tm);
	}
	PrintTimeDone(DST_INNER_1);

	PrintTime(DST_INNER_NAG1);
	for (int i = 0; i < count; i++)
	{
		temp_tm.tm_isdst = -1;
		mktime(&temp_tm);
	}
	PrintTimeDone(DST_INNER_NAG1);
/************************************/
  PrintTime(DST_OUTTER_0);
  temp_tm.tm_isdst = 0;
  for (int i = 0; i < count; i++)
	{
		mktime(&temp_tm);
	}
	PrintTimeDone(DST_OUTTER_0);

	PrintTime(DST_OUTTER_1);
  temp_tm.tm_isdst = 1;
  for (int i = 0; i < count; i++)
	{
		mktime(&temp_tm);
	}
	PrintTimeDone(DST_OUTTER_1);

	PrintTime(DST_OUTTER_NAG1);
  temp_tm.tm_isdst = -1;
  for (int i = 0; i < count; i++)
	{
		mktime(&temp_tm);
	}
	PrintTimeDone(DST_OUTTER_NAG1);
}

3.2.2 实验结果

$ ./a.out 10000
32位实验结果
imer_DST_INNER_0 spend time:3957 us
timer_DST_INNER_1 spend time:1986811 us
timer_DST_INNER_NAG1 spend time:3871 us
timer_DST_OUTTER_0 spend time:3889 us
timer_DST_OUTTER_1 spend time:4042 us
timer_DST_OUTTER_NAG1 spend time:3958 us

64位实验结果
timer_DST_INNER_0 spend time:2622 us
timer_DST_INNER_1 spend time:1408451 us
timer_DST_INNER_NAG1 spend time:2588 us
timer_DST_OUTTER_0 spend time:2534 us
timer_DST_OUTTER_1 spend time:2698 us
timer_DST_OUTTER_NAG1 spend time:2621 us

3.2.3 实验结果分析

从上面可以看出,当tm_isdst与系统夏令时设置不一致的时候,性能为一致时的1/500左右。为什么将tm_isdst设置放在外面的时候性能又高了很多呢?从源码可以看出,当mktime对isdst进行矫正后,将正确的isdst存到了传入的结构体中,所以第二次进来时,isdst与系统一致,不需要矫正。

**特别注意:**当tm_isdst初始化为-1的时候,mktime设置是否为夏令时时间时失手编译器的设计影响,所以传入isdst=-1可能会得到错误的结果,即非夏令时按照夏令时计算或者夏令时按照非夏令时计算。

4. 背景中出现的问题以及mktime使用注意事项

4.1 背景中问题原因

通过实验程序以及原程序片段分析,运行过程中,在32位机器上的tm_isdst默认值为0,在64位机器上tm_isdst的默认值为1(与系统和内存环境有关,可能出现不同结果,我的机器上上述问题),导致在32位机器上运行时间较短,而且第一次运行时需要设置TZ环境变量,故第一次运行时间为
51us,后面运行时间较短。而64为机器一致运行时间为150us左右。

4.2 mktime使用注意事项

  • 使用前建议设置TZ环境变量,在大量调用操作过程中,有助于将该部分性能提高到5~6倍.
  • mktime函数中传入的tm的tm_isdst参数必须设置为0或1,若设置为-1可能会得到错误的结果,或者使用默认值导致性能大大降低,国内一般设置为0,因为中国不采用夏令时。

5. 参考链接:

https://github.molgen.mpg.de/git-mirror/glibc/blob/20003c49884422da7ffbc459cdeee768a6fee07b/time/mktime.c
https://github.molgen.mpg.de/git-mirror/glibc/blob/20003c49884422da7ffbc459cdeee768a6fee07b/time/tzset.c
https://blog.csdn.net/aquester/article/details/54669264

猜你喜欢

转载自blog.csdn.net/u014630623/article/details/88992582