4
1

More than 1 year has passed since last update.

[Go] time.Time が encode & decode で元に戻らないので、time.Timeの中を調べてみた

Last updated at Posted at 2022-09-06

time.Time を encode して decode しても元に戻らない?!

time.Timegob の encode / decode を試していて驚いたことがありました。以下の実行結果、みなさんわかりますか?

main.go
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 == t2false になってしまうのです。確かに、t1.String()t2.String()m=+0.000100430 の有無という違いもあります。

別の方法で作った t3t2 は一致しているので、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 構造体の中身を見てみましょう。

time.go
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日を基準とする符号付きの秒数
  • hasMonotonic == 1 の場合
    • wall の値(上位bitから順に)
      • 1 bit: 1 (hasMonotonic)
      • 33 bit: 西暦1885年1月1日を基準とする符号なしの秒数
      • 30 bit: 0-999999999 までのナノ秒
    • ext の値
      • プロセス開始時からの符号付きのナノ秒 (monotonic clock)

実際の値を見てみましょう。unexported なフィールドなので、少し工夫して出力しています。

monotonic clock あり

典型的なパターンとして time.Now を利用しています。実際にビット演算すると計算しやすく作られていることも体感できると思います。

main.go
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 にそのままナノ秒が入っていることが確かにわかります。

main.go
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

上記のように wallext に monotonic clock の情報を入れ込んでいることがわかりました。

2157年問題?

hasMonotonic == 1 の場合のデータ構造を思い出すと気になることがあります。西暦1885年1月1日を基準とする符号なしの秒数 が33bitしかありません。これでは2157年頃までしか表現できないことになってしまいます。

実際の挙動はどうなっているのでしょうか。実験してみましょう。

main.go
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 では wallext というフィールドを用いて monotonic clock あり/なし のデータを管理している

実は、monotonic clock が Go に入ったのは 1.9 からで 1.8 までは普通(?)に secnsec で保持していました。time.Time の中身まで見て色々動かせば、time.Time の encode / decode に注意しないといけないことも自然に頭に入ったのではないでしょうか。


おまけ

time.Now の中身も見てみる

time.Now の中を monotonic clock に注目して見てみます。最初に runtime から mono を取得し、基準値となる startNano を差し引いて設定しています。3

time.go
// 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 同様のことが起きます。

main.go
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
  1. monotonic という単語は"単調な"という意味なので、"戻らない"というニュアンスが込められているようです。

  2. time.Since などで、monotonic clock が活躍します。「encode の際に消してもよいの?」という疑問が残りますが、時間の計測を行う間に time.Time の構造体を encode するようなユースケースはあまりなさそうです。あったとしても、長期間の計測になりそうですので、その場合はもはや wall clock で十分 or 別の方法で時間計測を行うべきなのではないかと考えます。

  3. 開始の基準値を実は 1ns だけ前にずらしている実装になっています(1.19.0 時点)。これは、①そもそも runtime clock の時間分解能がかなり低く(Windows 2008 で 15ms 程度とのこと)、②時間を計測したときに 0 になってしまうことを避けたいためにこのようになっているとのことです。こうすることで、プロセスが始まったタイミングで startNano に値が入るのでその後の処理で runtimeNano() > startNano が保証されるということです。

4
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
1