[インデックス 14673] ファイルの概要
このコミットは、Go言語のsyscall
パッケージにおけるPlan 9オペレーティングシステム向けの環境変数キャッシュの挙動を改善するものです。具体的には、環境変数のキャッシュを「遅延(lazy)」に初期化するように変更し、Getenv
関数の初回呼び出し時のシステムコール数を削減することを目的としています。
コミット
commit 2ad67147936bb1bf1f95d2a2ae41deab236712e9
Author: Anthony Martin <ality@pbrane.org>
Date: Mon Dec 17 08:33:51 2012 -0800
syscall: lazily populate the environment cache on Plan 9
This decreases the amount of system calls during the
first call to Getenv. Calling Environ will still read
in all environment variables and populate the cache.
R=golang-dev, rsc
CC=golang-dev
https://golang.org/cl/6939048
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/2ad67147936bb1bf1f95d2a2ae41deab236712e9
元コミット内容
このコミットは、src/pkg/syscall/env_plan9.go
ファイルに対して行われました。主な変更点は、環境変数をキャッシュするenv
マップの初期化と、Getenv
、Setenv
、Clearenv
、Environ
といった環境変数関連の関数におけるキャッシュの利用方法です。
変更前は、Getenv
、Setenv
、Clearenv
のいずれかが最初に呼び出された際に、sync.Once
を用いてcopyenv
関数が実行され、システム上の全ての環境変数が一度に読み込まれてenv
マップにキャッシュされていました。
変更の背景
この変更の背景には、パフォーマンスの最適化があります。従来のGoのsyscall
パッケージにおけるPlan 9環境での環境変数管理では、Getenv
(単一の環境変数を取得する関数)が初めて呼び出された際に、システム上の全ての環境変数を読み込み、内部キャッシュ(env
マップ)に格納していました。これは、copyenv
関数がsync.Once
によって一度だけ実行されることで実現されていました。
しかし、このアプローチには以下の問題がありました。
- 不要なシステムコール:
Getenv
が呼び出された際に、実際に必要とされるのは特定の1つの環境変数だけであるにもかかわらず、全ての環境変数を読み込むために多くのシステムコールが発生していました。これは、特に環境変数の数が多い場合にオーバーヘッドとなります。 - 遅延の発生: 初めて
Getenv
を呼び出す際に、全ての環境変数を読み込む処理が完了するまで待機する必要があり、アプリケーションの起動時や初回アクセス時に遅延が発生する可能性がありました。
このコミットは、この問題を解決するために、「遅延キャッシュ(lazy cache population)」というアプローチを導入しました。これにより、Getenv
が呼び出された際には、まずキャッシュをチェックし、存在しない場合にのみ、その特定の環境変数をシステムから読み込むように変更されました。全ての環境変数を一度に読み込むのは、Environ
関数(全ての環境変数をリストとして返す関数)が呼び出された場合に限定されるようになりました。
前提知識の解説
Plan 9 from Bell Labs
Plan 9 from Bell Labsは、ベル研究所で開発された分散オペレーティングシステムです。Unixの設計思想をさらに推し進め、全てのリソース(ファイル、デバイス、ネットワーク接続など)をファイルシステムとして表現するという特徴を持っています。環境変数も例外ではなく、Plan 9では/env
という特殊なディレクトリの下に、各環境変数がファイルとして存在します。例えば、PATH
環境変数の値は/env/PATH
というファイルを読み込むことで取得できます。
Go言語のsyscall
パッケージ
Go言語のsyscall
パッケージは、オペレーティングシステムが提供する低レベルなプリミティブ(システムコール)へのインターフェースを提供します。これにより、Goプログラムはファイル操作、プロセス管理、ネットワーク通信など、OS固有の機能に直接アクセスできます。このコミットが対象としているenv_plan9.go
は、Plan 9オペレーティングシステムに特化した環境変数関連のシステムコールをラップする部分です。
環境変数キャッシュ
プログラムが環境変数を頻繁に参照する場合、毎回OSに問い合わせる(システムコールを発行する)のは非効率です。そのため、多くのシステムでは、一度読み込んだ環境変数をメモリ上のデータ構造(キャッシュ)に保存し、次回の参照時にはキャッシュから取得することでパフォーマンスを向上させます。
sync.Once
Go言語のsync
パッケージに含まれるOnce
型は、特定の処理がプログラムの実行中に一度だけ実行されることを保証するためのユーティリティです。複数のゴルーチンから同時に呼び出されても、指定された関数は一度だけ実行され、その完了を待ってから他のゴルーチンが続行します。これは、初期化処理などでよく用いられます。
遅延初期化 (Lazy Initialization)
遅延初期化とは、リソースの初期化や計算を、そのリソースが実際に必要とされるまで遅らせるプログラミングパターンです。これにより、不要なリソースの消費や計算を避け、プログラムの起動時間やパフォーマンスを向上させることができます。このコミットでは、環境変数のキャッシュの「完全な」初期化を、Environ
が呼び出されるまで遅延させています。
技術的詳細
このコミットの核心は、Getenv
関数が環境変数を取得する際の動作変更と、それに伴うenv
キャッシュの管理方法の変更です。
-
env
マップの初期化: 変更前はcopyenv
関数内でenv = make(map[string]string)
としていましたが、変更後はvar env = make(map[string]string)
としてグローバル変数宣言時に初期化されるようになりました。これにより、マップ自体は常に利用可能な状態になります。 -
Getenv
の遅延キャッシュ読み込み:- 変更前:
Getenv
の冒頭でenvOnce.Do(copyenv)
が呼び出され、copyenv
が一度だけ実行されて全ての環境変数がキャッシュされていました。 - 変更後:
envOnce.Do(copyenv)
がGetenv
から削除されました。代わりに、Getenv
はまずenv
キャッシュ内にkey
が存在するかをチェックします。
もしキャッシュに存在しない場合、if v, ok := env[key]; ok { return v, true }
readenv(key)
を呼び出して、その特定の環境変数のみをPlan 9の/env
ファイルシステムから読み込みます。
これにより、v, err := readenv(key) if err != nil { return "", false } env[key] = v // 読み込んだ値をキャッシュに追加 return v, true
Getenv
の初回呼び出し時でも、必要な環境変数のみがシステムコールによって読み込まれ、キャッシュに追加されます。
- 変更前:
-
Environ
による完全キャッシュ読み込み:Environ
関数は、全ての環境変数を文字列スライスとして返します。この関数が呼び出された際に、envOnce.Do(copyenv)
が実行されるように変更されました。func Environ() []string { envLock.RLock() defer envLock.RUnlock() envOnce.Do(copyenv) // ここで全ての環境変数をキャッシュに読み込む a := make([]string, len(env)) i := 0 for k, v := range env { a[i] = k + "=" + v i++ } return a }
これにより、全ての環境変数を必要とする
Environ
が呼び出された場合にのみ、copyenv
が実行され、全ての環境変数がキャッシュに読み込まれるという、より効率的な動作が実現されます。 -
エラー変数の導入:
zero length key
とshort write
のエラーメッセージが、それぞれerrZeroLengthKey
とerrShortWrite
という変数として定義されました。これにより、エラーメッセージの再利用性と一貫性が向上します。 -
writeenv
の堅牢化:writeenv
関数において、Write
システムコールの戻り値である書き込みバイト数n
が、書き込もうとしたバイト列の長さlen(b)
と一致するかどうかのチェックが追加されました。これにより、部分的な書き込みが発生した場合にerrShortWrite
を返すことで、より堅牢なエラーハンドリングが可能になりました。
コアとなるコードの変更箇所
--- a/src/pkg/syscall/env_plan9.go
+++ b/src/pkg/syscall/env_plan9.go
@@ -12,14 +12,17 @@ import (
)
var (
- // envOnce guards initialization by copyenv, which populates env.
+ // envOnce guards copyenv, which populates env.
envOnce sync.Once
// envLock guards env.
envLock sync.RWMutex
// env maps from an environment variable to its value.
- env map[string]string
+ env = make(map[string]string)
+
+ errZeroLengthKey = errors.New("zero length key")
+ errShortWrite = errors.New("i/o count too small")
)
func readenv(key string) (string, error) {
@@ -47,12 +50,18 @@ func writeenv(key, value string) error {
return err
}
defer Close(fd)
- _, err = Write(fd, []byte(value))
- return err
+ b := []byte(value)
+ n, err := Write(fd, b)
+ if err != nil {
+ return err
+ }
+ if n != len(b) {
+ return errShortWrite
+ }
+ return nil
}
func copyenv() {
-\tenv = make(map[string]string)
fd, err := Open("/env", O_RDONLY)
if err != nil {
return
@@ -72,7 +81,6 @@ func copyenv() {
}
func Getenv(key string) (value string, found bool) {
-\tenvOnce.Do(copyenv)
if len(key) == 0 {
return "", false
}
@@ -80,17 +88,20 @@ func Getenv(key string) (value string, found bool) {
envLock.RLock()
defer envLock.RUnlock()
-\tv, ok := env[key]
-\tif !ok {\n+\tif v, ok := env[key]; ok {\n+\t\treturn v, true
+\t}\n+\tv, err := readenv(key)
+\tif err != nil {
return "", false
}
+\tenv[key] = v
return v, true
}
func Setenv(key, value string) error {
-\tenvOnce.Do(copyenv)
if len(key) == 0 {
-\t\treturn errors.New("zero length key")
+\t\treturn errZeroLengthKey
}
envLock.Lock()
@@ -105,8 +116,6 @@ func Setenv(key, value string) error {
}
func Clearenv() {
-\tenvOnce.Do(copyenv) // prevent copyenv in Getenv/Setenv
-\
envLock.Lock()
defer envLock.Unlock()
@@ -115,9 +124,10 @@ func Clearenv() {\n }
func Environ() []string {
-\tenvOnce.Do(copyenv)\n \tenvLock.RLock()\n \tdefer envLock.RUnlock()\n+\n+\tenvOnce.Do(copyenv)\n \ta := make([]string, len(env))\n \ti := 0\n \tfor k, v := range env {
コアとなるコードの解説
var
ブロックの変更
var (
- // envOnce guards initialization by copyenv, which populates env.
+ // envOnce guards copyenv, which populates env.
envOnce sync.Once
// envLock guards env.
envLock sync.RWMutex
// env maps from an environment variable to its value.
- env map[string]string
+ env = make(map[string]string)
+
+ errZeroLengthKey = errors.New("zero length key")
+ errShortWrite = errors.New("i/o count too small")
)
env
マップが宣言時にmake(map[string]string)
で初期化されるようになりました。これにより、copyenv
が呼び出される前にマップがnilである可能性がなくなります。また、新しいエラー変数errZeroLengthKey
とerrShortWrite
が導入され、エラーハンドリングの統一性が図られています。
writeenv
関数の変更
func writeenv(key, value string) error {
fd, err := Create("/env/" + key, O_WRONLY)
if err != nil {
return err
}
defer Close(fd)
- _, err = Write(fd, []byte(value))
- return err
+ b := []byte(value)
+ n, err := Write(fd, b)
+ if err != nil {
+ return err
+ }
+ if n != len(b) { // 書き込みバイト数のチェックを追加
+ return errShortWrite
+ }
+ return nil
}
Write
システムコールが実際に書き込んだバイト数n
をチェックし、書き込もうとしたバイト列の長さlen(b)
と異なる場合にerrShortWrite
を返すようになりました。これにより、部分的な書き込みエラーを検出できるようになり、堅牢性が向上しています。
copyenv
関数の変更
func copyenv() {
-\tenv = make(map[string]string) // この行が削除された
fd, err := Open("/env", O_RDONLY)
if err != nil {
return
env
マップの初期化がグローバル変数宣言時に行われるようになったため、copyenv
関数内でのenv = make(map[string]string)
の行が削除されました。
Getenv
関数の変更
func Getenv(key string) (value string, found bool) {
-\tenvOnce.Do(copyenv) // この行が削除された
if len(key) == 0 {
return "", false
}
envLock.RLock()
defer envLock.RUnlock()
-\tv, ok := env[key]
-\tif !ok {\n+\tif v, ok := env[key]; ok { // まずキャッシュをチェック
+\t\treturn v, true
+\t}
+\tv, err := readenv(key) // キャッシュにない場合のみシステムから読み込む
+\tif err != nil {
return "", false
}
+\tenv[key] = v // 読み込んだ値をキャッシュに追加
return v, true
}
Getenv
の冒頭にあったenvOnce.Do(copyenv)
が削除され、代わりにまずenv
キャッシュをチェックするロジックが追加されました。キャッシュにkey
が存在すればその値を返し、存在しなければreadenv(key)
を呼び出してシステムから直接読み込み、その値をキャッシュに追加してから返します。これが「遅延キャッシュ」の核心部分です。
Setenv
関数の変更
func Setenv(key, value string) error {
-\tenvOnce.Do(copyenv) // この行が削除された
if len(key) == 0 {
-\t\treturn errors.New("zero length key")
+\t\treturn errZeroLengthKey // 新しいエラー変数を使用
}
envLock.Lock()
Setenv
からもenvOnce.Do(copyenv)
が削除されました。また、errors.New("zero length key")
が新しく定義されたerrZeroLengthKey
に置き換えられました。
Clearenv
関数の変更
func Clearenv() {
-\tenvOnce.Do(copyenv) // この行が削除された
-\
envLock.Lock()
defer envLock.Unlock()
Clearenv
からもenvOnce.Do(copyenv)
が削除されました。
Environ
関数の変更
func Environ() []string {
-\tenvOnce.Do(copyenv) // この行が移動した
\tenvLock.RLock()
\tdefer envLock.RUnlock()
+\n+\tenvOnce.Do(copyenv) // ここで一度だけ全ての環境変数をキャッシュに読み込む
\ta := make([]string, len(env))\n \ti := 0\n \tfor k, v := range env {
Environ
関数内でenvOnce.Do(copyenv)
が呼び出されるように変更されました。これにより、全ての環境変数をリストとして取得する必要がある場合にのみ、copyenv
が実行され、全ての環境変数がキャッシュに読み込まれることが保証されます。
関連リンク
- Go言語の
sync.Once
に関する公式ドキュメントや解説 - Plan 9 from Bell Labsの環境変数に関するドキュメント(
/env
ファイルシステムについて)
参考にした情報源リンク
- Go言語の
sync
パッケージ - Plan 9 from Bell Labs
- Goの
syscall
パッケージ - GoのChange List 6939048 (コミットメッセージに記載されているリンク)
- Goの環境変数に関する議論や実装 (一般的な情報源として)
- Plan 9の環境変数に関するStack Overflowの議論など (一般的な情報源として)
- Goの
errors
パッケージ - Goの
io
パッケージ (Write関数の戻り値に関する一般的な情報) - Goの
os
パッケージ (環境変数に関する高レベルなAPI) - Goの
sync.RWMutex
に関する情報 (読み書きロックに関する一般的な情報) - Goの
map
に関する情報 (マップの初期化に関する一般的な情報)