【问题标题】:Why is gmtime implemented this way?为什么 gmtime 是这样实现的?
【发布时间】:2010-06-28 21:40:08
【问题描述】:

我偶然发现了 Minix 的 gmtime 函数的源代码。我对从纪元以来的天数计算年份数的位感兴趣。这是那一点的胆量:

http://www.raspberryginger.com/jbailey/minix/html/gmtime_8c-source.html

http://www.raspberryginger.com/jbailey/minix/html/loc__time_8h-source.html

#define EPOCH_YR 1970
#define LEAPYEAR(year) (!((year) % 4) && (((year) % 100) || !((year) % 400)))
#define YEARSIZE(year) (LEAPYEAR(year) ? 366 : 365)

int year = EPOCH_YR;

while (dayno >= YEARSIZE(year)) {
    dayno -= YEARSIZE(year);
    year++;
}

看起来算法是 O(n),其中 n 是与 epoch 的距离。此外,LEAPYEAR 似乎必须为每一年单独计算——当前日期计算数十次,远期日期计算更多。我有以下算法来做同样的事情(在这种情况下来自 ISO-9601 时代(Year 0 = 1 BC)而不是 UNIX 时代):

#define CYCLE_1   365
#define CYCLE_4   (CYCLE_1   *  4 + 1)
#define CYCLE_100 (CYCLE_4   * 25 - 1)
#define CYCLE_400 (CYCLE_100 *  4 + 1)

year += 400 * (dayno / CYCLE_400)
dayno = dayno % CYCLE_400

year += 100 * (dayno / CYCLE_100)
dayno = dayno % CYCLE_100

year +=   4 * (dayno / CYCLE_4)
dayno = dayno % CYCLE_4

year +=   1 * (dayno / CYCLE_1)
dayno = dayno % CYCLE_1

这对于任何日期都在 O(1) 中运行,并且看起来即使对于相当接近 1970 年的日期它也应该更快。

那么,假设 Minix 开发人员是聪明人,他们这样做是有原因的,并且可能比我更了解 C,为什么?

【问题讨论】:

  • 看起来应该更快。您需要考虑您的架构以及某些指令(如乘法)的速度以及您拥有的分支预测器有多好(大多数都非常好)。 @jim mcnamara 有一些有趣的结果。

标签: c time


【解决方案1】:

将您的代码作为 y2 minix 代码运行为 y1 Solaris 9 v245 并获得此分析器数据:

 %Time Seconds Cumsecs  #Calls   msec/call  Name
  79.1    0.34    0.34   36966      0.0092  _write
   7.0    0.03    0.37 1125566      0.0000  .rem
   7.0    0.03    0.40   36966      0.0008  _doprnt
   4.7    0.02    0.42 1817938      0.0000  _mcount
   2.3    0.01    0.43   36966      0.0003  y2
   0.0    0.00    0.43       4      0.      atexit
   0.0    0.00    0.43       1      0.      _exithandle
   0.0    0.00    0.43       1      0.      main
   0.0    0.00    0.43       1      0.      _fpsetsticky
   0.0    0.00    0.43       1      0.      _profil
   0.0    0.00    0.43   36966      0.0000  printf
   0.0    0.00    0.43  147864      0.0000  .div
   0.0    0.00    0.43   73932      0.0000  _ferror_unlocked
   0.0    0.00    0.43   36966      0.0000  memchr
   0.0    0.00    0.43       1      0.      _findbuf
   0.0    0.00    0.43       1      0.      _ioctl
   0.0    0.00    0.43       1      0.      _isatty
   0.0    0.00    0.43   73932      0.0000  _realbufend
   0.0    0.00    0.43   36966      0.0000  _xflsbuf
   0.0    0.00    0.43       1      0.      _setbufend
   0.0    0.00    0.43       1      0.      _setorientation
   0.0    0.00    0.43  137864      0.0000  _memcpy
   0.0    0.00    0.43       3      0.      ___errno
   0.0    0.00    0.43       1      0.      _fstat64
   0.0    0.00    0.43       1      0.      exit
   0.0    0.00    0.43   36966      0.0000  y1

也许这就是答案

【讨论】:

  • 似乎循环实现在 0 秒内运行,而模实现在 0.01 秒内运行。
  • 绝对是进行基准测试的案例。在看到这些结果之前,我会打赌提问者的代码会更快。
【解决方案2】:

这纯粹是推测,但也许 MINIX 有比执行速度更重要的要求,例如简单、易于理解和简洁?毕竟有些代码是印在教科书上的。

【讨论】:

    【解决方案3】:

    您的方法看起来不错,但要让它在 EPOCH_YR = 1970 上工作有点困难,因为您现在处于多个周期的中期。

    你能看看你是否有类似的情况,看看它是否仍然更好?

    您当然是对的,是否应该在任何高性能代码中使用 gmtime() 实现是值得商榷的。在任何紧密的循环中都需要做很多忙碌的工作。

    【讨论】:

    • 只需从 0..1970 年减去天数即可。这将是恒定的,可以存储为另一个#define。
    • 正如亚当所说,简单地减去偏移量就可以解决这个问题,尽管它会溢出 32 位时间戳。将纪元设置为公元 2000 年将解决这两个问题,需要在主计算之前和之后进行一次额外的操作。如果 64 位时间戳可用,则 ISO 零年可能会更好,因为它 a) 保存操作,b) 更明显,以及 c) 简单的日历无关的星期几。另一方面,如果你被现代时代的 32 位时间戳困住了,你可能会忘记 100 年和 400 年的周期,因为无论如何 2000 年是闰年,而 1900 年和 2100 年已经过去了范围。
    • @Thom Smith 2000 年的天数(~730k)远低于 2^32。两个代码片段都不关心秒数。
    • 我剪掉了其他部分;它们在链接中。 gmtime 函数接受一个标准的 UNIX 时间戳,从中派生出 dayno。
    • @Thom Smith:您可能会争辩说“ISO 零年可能更好”,但 POSIX 系统上的现有做法是将 time_t 重新定义为具有相同纪元的 64 位类型,因为这允许旧程序继续工作。
    【解决方案4】:

    正确的方法。你肯定想要一个 O(1) 算法。毫不费力地在玛雅历法中工作。检查最后一行:dayno 被限制为 0..364,尽管在闰年它需要在 0..365 范围内。前一行也有类似的缺陷。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2012-10-31
      • 2011-09-07
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多