西暦2262年問題に対処するべきか

西暦2038年問題はみなさんご存知ですよね。2038年1月19日午前3時14分7秒(UTC)を過ぎると 世界中のUNIXがばくはつする問題 time_t が符号付き32ビットなプログラムで現在時刻を正しく扱えなくなる問題です。

C言語の time_t は典型的にはUnix epoch(UTCで1970年1月1日午前0時)からの経過時間(うるう秒は考慮しない)を秒単位で保持しており、それが\(2^{31}-1\)に到達するのが2038年1月19日午前3時14分7秒(UTC)なわけですね。

2038年は割と近い将来なので、モダンなC処理系では time_t を64ビット整数にするなどの対応を行なって2038年問題を乗り切ろうとしています。

それでも、時刻を固定長整数で表現する限り、いつか限界が来ます。「time_t を64ビット整数にする」という対応は、問題を西暦2038年から西暦292277026596年に先送りしたに過ぎません。

そして、時刻の表現を「秒単位」ではなくもっと細かい単位にするとこの限界はもっと早くやってきます。この記事では、時刻の表現をどういう刻みで何ビットにすると限界がいつになるのかを検討してみます。

秒刻み

符号付き32ビット整数の限界\(2^{31}-1\)は2038年1月19日というのはすでに述べた通りです。

符号なし32ビット整数の場合\(2^{32}-1\)は2106年2月7日になります。ドラえもんができてもおかしくない時代ですね。

倍精度浮動小数点数で安全に表現できる限界(MAX_SAFE_INTEGER)\(2^{53}-1\)は285428751年11月12日になります。2億年。文明が億の単位の年数で持続するとやばそうですね。弥勒菩薩は56億7000万年後でしたっけ。

符号付き64ビット整数の場合\(2^{63}-1\)は292277026596年12月4日です。2922億年……?地球は間違いなく滅亡しているでしょう。恒星間移民に期待です。

ミリ秒刻み

たとえば、JavaScriptの Date はミリ秒刻みです。なのでミリ秒刻みだとどうなるか考えましょう。

符号付き32ビット整数の限界\(2^{31}-1\)は1970年1月25日です。1ヶ月も保たないってことですね。

JavaScriptの時刻は±8,640,000,000,000,000ミリ秒の範囲ということになっています。これは1970年のepochから数えると西暦275760年9月13日に相当します。

倍精度浮動小数点数で安全に表現できる限界(MAX_SAFE_INTEGER)\(2^{53}-1\)は287396年10月12日になります。28万年です。

符号付き64ビット整数の場合\(2^{63}-1\)は292278994年8月17日です。2.9億年ですか。

マイクロ秒刻み

マイクロ秒刻みだとどうなるか考えましょう。

倍精度浮動小数点数で安全に表現できる限界(MAX_SAFE_INTEGER)\(2^{53}-1\)は2255年6月5日になります。

符号付き64ビット整数の場合\(2^{63}-1\)は294247年1月10日です。29万年です。

ナノ秒刻み

Standard MLの Time.time 型の精度は実装依存ですが、インターフェース的にはナノ秒まで用意されています。そこで、内部表現をナノ秒刻みとした場合にどうなるか考えます。

倍精度浮動小数点数で安全に表現できる限界(MAX_SAFE_INTEGER)\(2^{53}-1\)は1970年4月15日になります。4ヶ月保ちませんでした。

符号付き64ビット整数の場合\(2^{63}-1\)は2262年4月11日です。これが記事タイトルの「西暦2262年問題」です。

LunarMLではどうするべきか

私が作っているStandard ML処理系LunarMLはLua 5.3バックエンドとLuaJITバックエンド、それにJavaScriptバックエンドを持ちます。現在の Time.time の実装は

  • Lua 5.3バックエンド:符号付き64ビット整数(Int64.int)、マイクロ秒刻み
    • 29万年
  • LuaJITバックエンド:符号付き54ビット整数(Int54.int)、マイクロ秒刻み
    • 2255年まで
  • JavaScriptバックエンド:符号付き54ビット整数(Int54.int)、ミリ秒刻み
    • 28万年

となっています。

しかし、バックエンドによって時刻の精度とかが違うのは気持ち悪いので、統一したいです。それで「ナノ秒刻みで符号付き64ビット整数はどうか」と思ったわけです。

2262年。微妙なところですね。現代の電子計算機文明は23世紀まで持続するのでしょうか。火の鳥未来編は西暦3000年を過ぎた時代が舞台でした。

Wikipediaの記述によると、「ナノ秒刻みで符号付き64ビット整数」を採用しているシステムはGo言語など、いくつかあるようです。なので、これは先人がいない選択ではありません。

それでも、2262年を過ぎると使えなくなるシステムというのは一抹の不安が残ります。LunarMLが成功するかはわかりませんが、不安は取り除いておきたいです。

JavaScriptバックエンドの場合は、標準で BigInt が使えるので、「64ビットの壁」はありません。単に「ナノ秒刻みの多倍長整数」とすれば良いです。

Lua (5.3/LuaJIT) バックエンドの場合は、標準の多倍長整数がありません。LunarMLが独自に用意したものがありますが、オーバーヘッド(速度、出力コード量)が気になります。つまり「64ビットの壁」があります。

一つの選択肢は、複数の固定長整数を併用することです。例えば、C言語の struct timespec は秒刻みのフィールドとナノ秒刻みのフィールドを持ちます。ナノ秒のフィールドは0以上\(10^9\)未満の整数を表現できれば良くて(つまり32ビット整数などで十分)、秒刻みのフィールドは54ビット整数や64ビット整数を使っておけば億単位の年が経過しない限りは十分です。

今のところ、Luaバックエンドでは「複数の固定長整数を併用」するのが現実的かなあと思っています。

計算に使ったプログラム

計算のロジックを自分で組むことも考えましたが、面倒なので既製のライブラリーを使いました。こんな感じです:

{- cabal:
build-depends: base, time
-}
{-# LANGUAGE GHC2021 #-}
import Data.Time
import Data.Time.Clock.POSIX

sec :: Integer -> UTCTime
sec x = posixSecondsToUTCTime $ secondsToNominalDiffTime $ fromInteger x

ms :: Integer -> UTCTime
ms x = posixSecondsToUTCTime $ secondsToNominalDiffTime $ fromInteger x / 1000

us :: Integer -> UTCTime
us x = posixSecondsToUTCTime $ secondsToNominalDiffTime $ fromInteger x / 10^6

ns :: Integer -> UTCTime
ns x = posixSecondsToUTCTime $ secondsToNominalDiffTime $ fromInteger x / 10^9

main = do
    putStr "2^31-1 [s]: "
    print $ sec $ 2^31 - 1
    putStr "2^32-1 [s]: "
    print $ sec $ 2^32 - 1
    putStr "2^53-1 [s]: "
    print $ sec $ 2^53 - 1
    putStr "2^63-1 [s]: "
    print $ sec $ 2^63 - 1
    putStr "2^31-1 [ms]: "
    print $ ms $ 2^31 - 1
    putStr "8_640_000_000_000_000 [ms]: "
    print $ ms 8_640_000_000_000_000
    putStr "2^53-1 [ms]: "
    print $ ms $ 2^53 - 1
    putStr "2^63-1 [ms]: "
    print $ ms $ 2^63 - 1
    putStr "2^53-1 [us]: "
    print $ us $ 2^53 - 1
    putStr "2^63-1 [us]: "
    print $ us $ 2^63 - 1
    putStr "2^53-1 [ns]: "
    print $ ns $ 2^53 - 1
    putStr "2^63-1 [ns]: "
    print $ ns $ 2^63 - 1

出力はこんな感じです:

2^31-1 [s]: 2038-01-19 03:14:07 UTC
2^32-1 [s]: 2106-02-07 06:28:15 UTC
2^53-1 [s]: 285428751-11-12 07:36:31 UTC
2^63-1 [s]: 292277026596-12-04 15:30:07 UTC
2^31-1 [ms]: 1970-01-25 20:31:23.647 UTC
8_640_000_000_000_000 [ms]: 275760-09-13 00:00:00 UTC
2^53-1 [ms]: 287396-10-12 08:59:00.991 UTC
2^63-1 [ms]: 292278994-08-17 07:12:55.807 UTC
2^53-1 [us]: 2255-06-05 23:47:34.740991 UTC
2^63-1 [us]: 294247-01-10 04:00:54.775807 UTC
2^53-1 [ns]: 1970-04-15 05:59:59.254740991 UTC
2^63-1 [ns]: 2262-04-11 23:47:16.854775807 UTC

ちなみに、今回使ったHaskellのtimeパッケージの内部表現はピコ秒刻みの多倍長整数のようです。