time.Time を encode して decode しても元に戻らない?!
time.Time
の gob の encode / decode を試していて驚いたことがありました。以下の実行結果、みなさんわかりますか?
package main
import (
"fmt"
"time"
)
func main() {
t1 := time.Now()
b, _ := t1.GobEncode()
t2 := time.Unix(0, 0)
t2.GobDecode(b)
t3 := time.Now()
t3.GobDecode(b)
fmt.Printf("t1: %s\nt2: %s\nt3: %s\n", t1.String(), t2.String(), t3.String())
fmt.Printf("t1 == t2: %t\n", t1 == t2)
fmt.Printf("t2 == t3: %t\n", t2 == t3)
}
t1: 2022-09-06 23:39:28.780771 +0900 JST m=+0.000100430
t2: 2022-09-06 23:39:28.780771 +0900 JST
t3: 2022-09-06 23:39:28.780771 +0900 JST
t1 == t2: false
t2 == t3: true
なんと、encode して decode しただけなのに、t1 == t2
が false
になってしまうのです。確かに、t1.String()
と t2.String()
も m=+0.000100430
の有無という違いもあります。
別の方法で作った t3
と t2
は一致しているので、t2
の作成方法が悪いわけではなさそうです。
これはいったいなぜでしょうか?
monotonic clock
現在時刻を知るために wall clock というものを我々は使っています。一方 monotonic clock というのは、時間を計測するために利用される時計です。wall clock は OS の時刻変更をすることで時間の巻戻りが発生することがありますが、monotonic clockではそのようなケースでも適切に動くように作られています。 1
実は、 m=+0.000100430
の部分がその monotonic clock の値で、 encode の際にこの値は削除されているのです。そのため、decode で元に戻したとしても monotonic clock の情報が消えており、"元に戻らない"という現象が起きるのです。 monotonic clock は時間を計測するためのものであるというユースケースを考えると encode で値が削除されるのも納得感があります。2
では、この monotonic clock はどのように表現されているのでしょうか? time.Time
構造体の中身を見てみましょう。
type Time struct {
// wall and ext encode the wall time seconds, wall time nanoseconds,
// and optional monotonic clock reading in nanoseconds.
//
// From high to low bit position, wall encodes a 1-bit flag (hasMonotonic),
// a 33-bit seconds field, and a 30-bit wall time nanoseconds field.
// The nanoseconds field is in the range [0, 999999999].
// If the hasMonotonic bit is 0, then the 33-bit field must be zero
// and the full signed 64-bit wall seconds since Jan 1 year 1 is stored in ext.
// If the hasMonotonic bit is 1, then the 33-bit field holds a 33-bit
// unsigned wall seconds since Jan 1 year 1885, and ext holds a
// signed 64-bit monotonic clock reading, nanoseconds since process start.
wall uint64
ext int64
// loc specifies the Location that should be used to
// determine the minute, hour, month, day, and year
// that correspond to this Time.
// The nil location means UTC.
// All UTC times are represented with loc==nil, never loc==&utcLoc.
loc *Location
}
hasMonotonic
というフラグで管理しているようです。コメントから以下のようなデータ構造になっていることがわかります。
-
hasMonotonic == 0
の場合- wall の値(上位bitから順に)
- 1 bit: 0 (hasMonotonic)
- 33 bit: すべての0
- 30 bit: 0-999999999 までのナノ秒
- ext の値
- 西暦1年1月1日を基準とする符号付きの秒数
- wall の値(上位bitから順に)
-
hasMonotonic == 1
の場合- wall の値(上位bitから順に)
- 1 bit: 1 (hasMonotonic)
- 33 bit: 西暦1885年1月1日を基準とする符号なしの秒数
- 30 bit: 0-999999999 までのナノ秒
- ext の値
- プロセス開始時からの符号付きのナノ秒 (monotonic clock)
- wall の値(上位bitから順に)
実際の値を見てみましょう。unexported なフィールドなので、少し工夫して出力しています。
monotonic clock あり
典型的なパターンとして time.Now
を利用しています。実際にビット演算すると計算しやすく作られていることも体感できると思います。
package main
import (
"fmt"
"reflect"
"time"
)
func main() {
t1 := time.Now()
elem := reflect.ValueOf(&t1).Elem()
wall := elem.FieldByName(`wall`).Uint()
ext := elem.FieldByName(`ext`).Int()
fmt.Printf("wall: %d\n", wall)
fmt.Printf("ext: %d\n", ext)
if hasMonotonic(wall) {
fmt.Printf("secs from 1885/01/01: %d\n", ((1<<63)^wall)>>30)
fmt.Printf("nano secs: %d\n", (wall<<34)>>34)
} else {
fmt.Printf("nano secs: %d\n", wall)
}
fmt.Printf("secs from 1885/01/01(time.Time): %d\n", t1.Unix()-time.Date(1885, 1, 1, 0, 0, 0, 0, time.UTC).Unix())
fmt.Printf("nano secs(time.Time): %d\n", t1.Nanosecond())
}
func hasMonotonic(wall uint64) bool {
return (wall>>63)&1 == 1
}
wall: 13888534293255422360
ext: 143687
secs from 1885/01/01: 4344770923
nano secs: 676463000
secs from 1885/01/01(time.Time): 4344770923
nano secs(time.Time): 676463000
monotonic clock なし
あえて time.Unix
を利用することで monotonic clock なしの値を生成しています。
こちらは wall
にそのままナノ秒が入っていることが確かにわかります。
package main
import (
"fmt"
"reflect"
"time"
)
func main() {
now := time.Now()
t1 := time.Unix(now.Unix(), int64(now.Nanosecond()))
elem := reflect.ValueOf(&t1).Elem()
wall := elem.FieldByName(`wall`).Uint()
ext := elem.FieldByName(`ext`).Int()
fmt.Printf("wall: %d\n", wall)
fmt.Printf("ext: %d\n", ext)
if hasMonotonic(wall) {
fmt.Printf("secs from 1885/01/01: %d\n", ((1<<63)^wall)>>30)
fmt.Printf("nano secs: %d\n", (wall<<34)>>34)
} else {
fmt.Printf("nano secs: %d\n", wall)
}
fmt.Printf("secs from 1885/01/01(time.Time): %d\n", t1.Unix()-time.Date(1885, 1, 1, 0, 0, 0, 0, time.UTC).Unix())
fmt.Printf("nano secs(time.Time): %d\n", t1.Nanosecond())
}
wall: 13888534293255422360
ext: 143687
secs from 1885/01/01: 4344770923
nano secs: 676463000
secs from 1885/01/01(time.Time): 4344770923
nano secs(time.Time): 676463000
上記のように wall
と ext
に monotonic clock の情報を入れ込んでいることがわかりました。
2157年問題?
hasMonotonic == 1
の場合のデータ構造を思い出すと気になることがあります。西暦1885年1月1日を基準とする符号なしの秒数
が33bitしかありません。これでは2157年頃までしか表現できないことになってしまいます。
実際の挙動はどうなっているのでしょうか。実験してみましょう。
package main
import (
"fmt"
"time"
)
func main() {
t1 := time.Now()
for i := 0; i < 100; i++ {
t1 = t1.Add(100000 * time.Hour)
fmt.Printf("t1: %s\n", t1.String())
}
}
t1: 2034-02-02 18:44:08.473612 +0900 JST m=+360000000.000099658
t1: 2045-07-01 10:44:08.473612 +0900 JST m=+720000000.000099658
t1: 2056-11-27 02:44:08.473612 +0900 JST m=+1080000000.000099658
t1: 2068-04-24 18:44:08.473612 +0900 JST m=+1440000000.000099658
t1: 2079-09-21 10:44:08.473612 +0900 JST m=+1800000000.000099658
t1: 2091-02-17 02:44:08.473612 +0900 JST m=+2160000000.000099658
t1: 2102-07-16 18:44:08.473612 +0900 JST m=+2520000000.000099658
t1: 2113-12-12 10:44:08.473612 +0900 JST m=+2880000000.000099658
t1: 2125-05-10 02:44:08.473612 +0900 JST m=+3240000000.000099658
t1: 2136-10-05 18:44:08.473612 +0900 JST m=+3600000000.000099658
t1: 2148-03-03 10:44:08.473612 +0900 JST m=+3960000000.000099658
t1: 2159-07-31 02:44:08.473612 +0900 JST
t1: 2170-12-26 18:44:08.473612 +0900 JST
...
2157年頃を境に monotonic clock の情報がなくなっていることがわかります。2157年を超えてもパニックにはならず、きちんとフォールバックするようになっていることも確認できました。
まとめ
-
time.Time
には wall clock だけでなく、 monotonic clock の情報が含まれている - monotonic clock の情報は encode されるタイミングで消失してしまう
- encode & decode 後には monotonic clock がない状態になるので、構造体としては元に戻らなくなってしまうので注意
-
time.Time
ではwall
とext
というフィールドを用いて monotonic clock あり/なし のデータを管理している
実は、monotonic clock が Go に入ったのは 1.9 からで 1.8 までは普通(?)に sec
と nsec
で保持していました。time.Time の中身まで見て色々動かせば、time.Time
の encode / decode に注意しないといけないことも自然に頭に入ったのではないでしょうか。
おまけ
time.Now の中身も見てみる
time.Now
の中を monotonic clock に注目して見てみます。最初に runtime から mono
を取得し、基準値となる startNano
を差し引いて設定しています。3
// Provided by package runtime.
func now() (sec int64, nsec int32, mono int64)
// runtimeNano returns the current value of the runtime clock in nanoseconds.
//go:linkname runtimeNano runtime.nanotime
func runtimeNano() int64
// Monotonic times are reported as offsets from startNano.
// We initialize startNano to runtimeNano() - 1 so that on systems where
// monotonic time resolution is fairly low (e.g. Windows 2008
// which appears to have a default resolution of 15ms),
// we avoid ever reporting a monotonic time of 0.
// (Callers may want to use 0 as "time not set".)
var startNano int64 = runtimeNano() - 1
// Now returns the current local time.
func Now() Time {
sec, nsec, mono := now()
mono -= startNano
sec += unixToInternal - minWall
if uint64(sec)>>33 != 0 {
return Time{uint64(nsec), sec + minWall, Local}
}
return Time{hasMonotonic | uint64(sec)<<nsecShift | uint64(nsec), mono, Local}
}
json の encode / decode
json の encode & decode でも gob 同様のことが起きます。
package main
import (
"encoding/json"
"fmt"
"time"
)
func main() {
t1 := time.Now()
b, _ := json.Marshal(t1)
var t2 time.Time
json.Unmarshal(b, &t2)
fmt.Printf("t1: %s\nt2: %s\n", t1.String(), t2.String())
fmt.Printf("t1 == t2: %t\n", t1 == t2)
}
t1: 2022-09-06 23:51:08.263018 +0900 JST m=+0.000085313
t2: 2022-09-06 23:51:08.263018 +0900 JST
t1 == t2: false
-
monotonic という単語は"単調な"という意味なので、"戻らない"というニュアンスが込められているようです。 ↩
-
time.Since
などで、monotonic clock が活躍します。「encode の際に消してもよいの?」という疑問が残りますが、時間の計測を行う間にtime.Time
の構造体を encode するようなユースケースはあまりなさそうです。あったとしても、長期間の計測になりそうですので、その場合はもはや wall clock で十分 or 別の方法で時間計測を行うべきなのではないかと考えます。 ↩ -
開始の基準値を実は 1ns だけ前にずらしている実装になっています(1.19.0 時点)。これは、①そもそも runtime clock の時間分解能がかなり低く(Windows 2008 で 15ms 程度とのこと)、②時間を計測したときに 0 になってしまうことを避けたいためにこのようになっているとのことです。こうすることで、プロセスが始まったタイミングで
startNano
に値が入るのでその後の処理でruntimeNano() > startNano
が保証されるということです。 ↩