[インデックス 14466] ファイルの概要
このコミットは、Go言語の標準ライブラリtime
パッケージにおけるNow()
およびUnixNano()
関数のメモリ割り当て(malloc)テストとベンチマークの追加に関するものです。特に、これらの関数がヒープメモリを割り当てないことを確認し、将来的な変更によるパフォーマンスへの影響を未然に防ぐことを目的としています。
コミット
commit 1e9ab9e7926ec655850379b0326b11f830a482e7
Author: Dave Cheney <dave@cheney.net>
Date: Sun Nov 25 11:29:06 2012 +1100
time: add Now()/UnixNano() malloc tests
The fix for issue 4403 may include more calls to time.Now().UnixNano(). I was concerned that if this function allocated it would cause additional garbage on the heap. It turns out that it doesn't, which is a nice surprise.
Also add benchmark for Now().UnixNano()
R=bradfitz, minux.ma
CC=golang-dev
https://golang.org/cl/6849097
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/1e9ab9e7926ec655850379b0326b11f830a482e7
元コミット内容
time: add Now()/UnixNano() malloc tests
このコミットは、time.Now().UnixNano()
への呼び出しが増える可能性のあるIssue 4403の修正に関連して、この関数がメモリを割り当てることでヒープ上に余分なガベージを生成するのではないかという懸念に対処するために、メモリ割り当てテストを追加するものです。結果として、この関数がメモリを割り当てないことが確認され、これは良い驚きであったと述べられています。また、Now().UnixNano()
のベンチマークも追加されています。
変更の背景
この変更の背景には、Go言語のtime
パッケージにおけるNow()
およびUnixNano()
関数のパフォーマンス特性、特にメモリ割り当てに関する懸念がありました。コミットメッセージによると、「Issue 4403の修正には、time.Now().UnixNano()
へのより多くの呼び出しが含まれる可能性がある」とのことです。
Go言語では、メモリ割り当て(アロケーション)はガベージコレクション(GC)のトリガーとなり、GCはアプリケーションの実行を一時停止させる可能性があるため、パフォーマンスに大きな影響を与えます。特に、頻繁に呼び出される関数が不必要にメモリを割り当てる場合、それはヒープ上に「ガベージ(ゴミ)」を生成し、GCの頻度と負荷を増加させ、結果としてアプリケーションのスループットやレイテンシを悪化させる可能性があります。
コミットの作者は、time.Now().UnixNano()
がメモリを割り当てるかどうかについて懸念を抱いていました。もし割り当てが発生すれば、Issue 4403の修正によってこの関数の呼び出しが増えることで、Goプログラム全体のパフォーマンスが低下する恐れがあったため、その振る舞いを検証する必要がありました。このコミットは、その懸念を解消し、Now()
およびUnixNano()
がメモリを割り当てないことをテストによって確認するために導入されました。
前提知識の解説
Go言語のメモリ管理とガベージコレクション (GC)
Go言語は自動メモリ管理を採用しており、開発者が手動でメモリを解放する必要はありません。Goランタイムにはガベージコレクタが組み込まれており、不要になったメモリ領域を自動的に回収します。
- ヒープ (Heap): プログラムが実行時に動的にメモリを割り当てる領域です。
make
やnew
などの関数を使って作成されたデータ構造や、関数の呼び出し間で寿命が続く可能性のあるデータはヒープに割り当てられます。 - スタック (Stack): 関数の呼び出しやローカル変数の格納に使われるメモリ領域です。スタックはLIFO(Last-In, First-Out)の原則で動作し、関数の呼び出しとリターンに伴って自動的にメモリが確保・解放されます。スタックに割り当てられたメモリはGCの対象外です。
- メモリ割り当て (Allocation): プログラムがヒープからメモリを要求する操作です。メモリ割り当てはコストのかかる操作であり、頻繁に行われるとパフォーマンスに影響を与えます。
- ガベージコレクション (GC): ヒープ上に存在する、もはやプログラムから到達できない(参照されていない)メモリ領域を特定し、解放するプロセスです。GoのGCは並行(concurrent)かつ低遅延(low-latency)を目指していますが、それでもGCが実行される際にはCPUリソースを消費し、場合によってはアプリケーションの実行が一時的に停止(ストップ・ザ・ワールド)することがあります。したがって、不必要なメモリ割り当てを減らすことは、GCの負荷を軽減し、アプリケーションのパフォーマンスを向上させる上で非常に重要です。
time
パッケージのNow()
とUnixNano()
time.Now()
: 現在のローカル時刻をtime.Time
型の値として返します。time.Time
型は、特定の時点を表す構造体です。time.UnixNano()
:time.Time
型のメソッドで、その時刻をUnixエポック(1970年1月1日UTC)からの経過ナノ秒数としてint64
型で返します。
これらの関数は、時刻の取得という非常に基本的な操作を行うため、Goプログラムの様々な場所で頻繁に呼び出される可能性があります。そのため、これらの関数のパフォーマンス特性、特にメモリ割り当ての有無は、Goアプリケーション全体のパフォーマンスに直接影響します。
runtime
パッケージとMemStats
Go言語のruntime
パッケージは、Goランタイムシステムとのインタラクションを可能にする低レベルの機能を提供します。
runtime.MemStats
: Goプログラムのメモリ割り当て統計に関する情報を含む構造体です。この構造体には、ヒープの使用状況、GCの統計、そしてメモリ割り当ての回数など、様々なメトリクスが含まれています。runtime.ReadMemStats(m *MemStats)
: 現在のメモリ統計をm
に書き込む関数です。この関数を呼び出すことで、特定の時点でのメモリ使用状況や割り当て回数を取得できます。MemStats.Mallocs
:MemStats
構造体に含まれるフィールドの一つで、ヒープに割り当てられたオブジェクトの総数を表します。この値は、プログラムの実行中にヒープ割り当てがどれだけ行われたかを追跡するために使用できます。
runtime.GOMAXPROCS
runtime.GOMAXPROCS(n int)
: Goランタイムが同時に実行できるOSスレッドの最大数を設定します。runtime.GOMAXPROCS(1)
と設定することで、Goスケジューラが同時に実行するOSスレッドを1つに制限します。これは、メモリ割り当てテストのような、実行環境の変動が結果に影響を与えやすいテストにおいて、より安定した結果を得るために使用されます。特に、並行処理によるメモリ割り当てのタイミングのずれを排除し、純粋な関数呼び出しによる割り当てを測定するのに役立ちます。
技術的詳細
このコミットで追加されたテストは、time.Now()
とtime.Now().UnixNano()
がメモリを割り当てないことを検証するために、runtime
パッケージのMemStats
構造体を利用しています。
テストの基本的なアプローチは以下の通りです。
runtime.GOMAXPROCS(runtime.GOMAXPROCS(1))
: テストの開始時にGOMAXPROCS
を1に設定し、テスト終了時に元の値に戻します。これにより、テストが単一のOSスレッドで実行されることが保証され、並行処理によるメモリ割り当ての不確実性が排除されます。これは、正確なメモリ割り当て数を測定するために重要です。mallocTest
スライス:{0, "time.Now()", func() { t = Now() }}
:time.Now()
の呼び出しが0回のメモリ割り当てを期待することを示します。{0, "time.Now().UnixNano()", func() { u = Now().UnixNano() }}
:time.Now().UnixNano()
の呼び出しが0回のメモリ割り当てを期待することを示します。count
フィールドは期待される平均メモリ割り当て数、desc
はテストの説明、fn
はテスト対象の関数呼び出しを含む匿名関数です。
TestCountMallocs
関数:- 各テストケース(
mt
)に対して、N
回(ここでは100回)の関数呼び出しを行います。 - 関数呼び出しの前後で
runtime.ReadMemStats
を呼び出し、MemStats.Mallocs
の値を記録します。 mallocs := 0 - memstats.Mallocs
で初期の割り当て数を記録し、ループ後にmallocs += memstats.Mallocs
で最終的な割り当て数を加算することで、ループ内での純粋な割り当て数を計算します。mallocs/N > uint64(mt.count)
という条件で、1回あたりの平均メモリ割り当て数が期待値(0)を超えていないかを検証します。もし超えていれば、テストは失敗します。
- 各テストケース(
このテストは、time.Now()
とtime.Now().UnixNano()
がヒープメモリを割り当てないという重要なパフォーマンス特性を保証します。これは、これらの関数が頻繁に呼び出されるGoアプリケーションにおいて、ガベージコレクションのオーバーヘッドを最小限に抑える上で不可欠です。
また、このコミットではBenchmarkNowUnixNano
という新しいベンチマークも追加されています。これは、time.Now().UnixNano()
の実行時間を測定し、そのパフォーマンスを評価するために使用されます。ベンチマークは、関数の速度を定量的に測定し、将来の変更がパフォーマンスに与える影響を追跡するために重要です。
コアとなるコードの変更箇所
変更はsrc/pkg/time/time_test.go
ファイルに集中しています。
--- a/src/pkg/time/time_test.go
+++ b/src/pkg/time/time_test.go
@@ -10,6 +10,7 @@ import (
"encoding/json"
"fmt"
"math/rand"
+ "runtime"
"strconv"
"strings"
"testing"
@@ -1037,9 +1038,47 @@ func TestParseDurationRoundTrip(t *testing.T) {
}
}
+var (
+ t Time
+ u int64
+)
+
+var mallocTest = []struct {
+ count int
+ desc string
+ fn func()
+}{
+ {0, `time.Now()`, func() { t = Now() }},\n\t{0, `time.Now().UnixNano()`, func() { u = Now().UnixNano() }},\n}
+
+func TestCountMallocs(t *testing.T) {
+ defer runtime.GOMAXPROCS(runtime.GOMAXPROCS(1))\n\tfor _, mt := range mallocTest {\n\t\tconst N = 100\n\t\tmemstats := new(runtime.MemStats)\n\t\truntime.ReadMemStats(memstats)\n\t\tmallocs := 0 - memstats.Mallocs\n\t\tfor i := 0; i < N; i++ {\n\t\t\tmt.fn()\n\t\t}\n\t\truntime.ReadMemStats(memstats)\n\t\tmallocs += memstats.Mallocs\n\t\tif mallocs/N > uint64(mt.count) {\n\t\t\tt.Errorf(\"%s: expected %d mallocs, got %d\", mt.count, mallocs/N)\n\t\t}\n\t}\n}
+
func BenchmarkNow(b *testing.B) {
for i := 0; i < b.N; i++ {\n-\t\tNow()\n+\t\tt = Now()\n+\t}\n+}\n+\n+func BenchmarkNowUnixNano(b *testing.B) {\n+\tfor i := 0; i < b.N; i++ {\n+\t\tu = Now().UnixNano()\n }\n }
コアとなるコードの解説
src/pkg/time/time_test.go
-
import "runtime"
の追加:runtime
パッケージがインポートされ、runtime.MemStats
やruntime.GOMAXPROCS
などの機能が利用可能になります。 -
グローバル変数の宣言:
var ( t Time u int64 )
t
はtime.Time
型の変数で、Now()
の戻り値を格納するために使用されます。u
はint64
型の変数で、Now().UnixNano()
の戻り値を格納するために使用されます。これらの変数は、テスト対象の関数呼び出しが最適化によって削除されないように、結果を保持するために必要です。 -
mallocTest
スライスの定義:var mallocTest = []struct { count int desc string fn func() }{ {0, `time.Now()`, func() { t = Now() }}, {0, `time.Now().UnixNano()`, func() { u = Now().UnixNano() }}, }
これは、メモリ割り当てテストの各ケースを定義する構造体のスライスです。
count
: 期待される平均メモリ割り当て数。ここでは両方とも0
に設定されており、メモリ割り当てが発生しないことを期待しています。desc
: テストケースの説明文字列。fn
: テスト対象の関数呼び出しを含む匿名関数。t = Now()
とu = Now().UnixNano()
は、それぞれの関数を呼び出し、結果をグローバル変数に代入することで、コンパイラによる最適化(関数呼び出しの削除など)を防ぎます。
-
TestCountMallocs
関数の追加:func TestCountMallocs(t *testing.T) { defer runtime.GOMAXPROCS(runtime.GOMAXPROCS(1)) // GOMAXPROCSを1に設定し、テスト終了時に元に戻す for _, mt := range mallocTest { const N = 100 // 各テストケースを100回実行 memstats := new(runtime.MemStats) // MemStats構造体を初期化 runtime.ReadMemStats(memstats) // 最初のMemStatsを読み込む mallocs := 0 - memstats.Mallocs // 初期割り当て数を記録 (負の値で開始) for i := 0; i < N; i++ { mt.fn() // テスト対象の関数を呼び出す } runtime.ReadMemStats(memstats) // 2回目のMemStatsを読み込む mallocs += memstats.Mallocs // 最終割り当て数を加算 if mallocs/N > uint64(mt.count) { // 平均割り当て数が期待値を超えているかチェック t.Errorf("%s: expected %d mallocs, got %d", mt.desc, mt.count, mallocs/N) } } }
この関数は、
mallocTest
スライス内の各テストケースを反復処理し、runtime.MemStats
を使用してメモリ割り当て数を測定します。mallocs
の計算は、ループの開始時と終了時のMemStats.Mallocs
の差分を取ることで、ループ内で発生した純粋なメモリ割り当て数を正確に把握するための一般的な手法です。 -
BenchmarkNow
の修正:- Now() + t = Now()
BenchmarkNow
関数内で、Now()
の戻り値がグローバル変数t
に代入されるように変更されました。これは、ベンチマーク中にコンパイラがNow()
の呼び出しを最適化して削除してしまうのを防ぐためです。結果が使用されない場合、コンパイラは関数呼び出しを最適化して削除する可能性があるため、正確なベンチマーク結果を得るためにはこの変更が必要です。 -
BenchmarkNowUnixNano
関数の追加:func BenchmarkNowUnixNano(b *testing.B) { for i := 0; i < b.N; i++ { u = Now().UnixNano() } }
Now().UnixNano()
のパフォーマンスを測定するための新しいベンチマーク関数が追加されました。同様に、戻り値はグローバル変数u
に代入され、最適化を防ぎます。
これらの変更により、time.Now()
とtime.Now().UnixNano()
がメモリを割り当てないことがテストによって保証され、またこれらの関数のパフォーマンスを継続的に監視するためのベンチマークが提供されます。
関連リンク
- Go言語の
time
パッケージのドキュメント: https://pkg.go.dev/time - Go言語の
runtime
パッケージのドキュメント: https://pkg.go.dev/runtime - Go言語のメモリ管理に関する公式ブログ記事やドキュメント(一般的な情報源として)
参考にした情報源リンク
- Go言語の公式ドキュメント
- Go言語のソースコード
- Go言語のガベージコレクションに関する一般的な情報源(例: Goブログ、技術記事)
runtime.MemStats
とメモリ割り当てテストに関するGoコミュニティの議論や例