[インデックス 11532] ファイルの概要
このコミットは、Go言語のsyscall
パッケージにおけるPlan 9環境での環境変数管理メカニズムを改善するものです。具体的には、環境変数をキャッシュする機能を追加し、環境変数へのアクセス時に発生するシステムコール数を大幅に削減することを目的としています。これにより、環境変数を頻繁に参照するプログラムのパフォーマンスが向上します。
コミット
commit 1583931bcf522c4128087f0fb7dc84c4caa2af28
Author: Anthony Martin <ality@pbrane.org>
Date: Tue Jan 31 18:14:02 2012 -0800
syscall: cache environment variables on Plan 9.
This can drastically reduce the number of system
calls made by programs that repeatedly query the
environment.
R=golang-dev, rsc
CC=golang-dev
https://golang.org/cl/5599054
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/1583931bcf522c4128087f0fb7dc84c4caa2af28
元コミット内容
このコミットは、Go言語のsyscall
パッケージ内のenv_plan9.go
ファイルに対して行われた変更です。主な目的は、Plan 9オペレーティングシステム上での環境変数へのアクセスを最適化することです。以前の実装では、環境変数を取得するたびにファイルシステムへのアクセス(システムコール)が発生していました。この変更により、環境変数をメモリ上にキャッシュすることで、繰り返し環境変数を参照する際のシステムコール数を劇的に削減し、パフォーマンスを向上させます。
変更の背景
Plan 9オペレーティングシステムでは、「全てがファイルである」という哲学が徹底されており、環境変数も例外ではありません。各環境変数は/env
ディレクトリ内のファイルとして表現されます。例えば、FOO
という環境変数の値を取得するには、/env/FOO
というファイルを開いてその内容を読み取るという操作が必要になります。
Go言語のsyscall
パッケージは、オペレーティングシステムの低レベルな機能にアクセスするためのインターフェースを提供します。Plan 9環境におけるsyscall
パッケージのGetenv
関数は、この/env
ディレクトリのファイルシステム操作を直接行っていました。
しかし、環境変数を頻繁に読み取るようなアプリケーション(例えば、設定値を繰り返し参照する、あるいは多数のプロセスを起動するようなケース)では、このファイルシステムアクセスがボトルネックとなる可能性がありました。ファイルシステムアクセスは、メモリからのデータ読み出しに比べてはるかにコストの高い操作であり、システムコールを伴うため、CPUオーバーヘッドも大きくなります。
このコミットは、このようなパフォーマンス上の課題を解決するために導入されました。環境変数を一度読み込んでメモリ上にキャッシュすることで、2回目以降のアクセスでは高速なメモリ読み出しで対応できるようになり、システムコール数を削減することが可能になります。
前提知識の解説
- Plan 9 from Bell Labs: ベル研究所で開発された分散オペレーティングシステムです。その設計思想の核となるのは「全てがファイルである」という原則で、デバイス、プロセス、ネットワークリソースなど、あらゆるものがファイルシステムを通じてアクセスされます。環境変数も
/env
という特殊なディレクトリ内のファイルとして扱われます。 - Go言語の
syscall
パッケージ: Go標準ライブラリの一部で、オペレーティングシステムが提供するシステムコールへの低レベルなインターフェースを提供します。これにより、GoプログラムはOSのカーネル機能に直接アクセスできます。 - 環境変数: オペレーティングシステムがプロセスに提供する、キーと値のペアからなる設定情報です。プログラムの動作を外部から制御するために広く利用されます。
- システムコール: ユーザー空間で動作するプログラムが、カーネル空間で動作するオペレーティングシステムのサービス(ファイルI/O、メモリ管理、プロセス管理など)を要求するためのメカニズムです。システムコールは、ユーザーモードからカーネルモードへのコンテキストスイッチを伴うため、比較的コストの高い操作です。
- キャッシュ: 頻繁にアクセスされるデータを、より高速なストレージ(この場合はメモリ)に一時的に保存しておくことで、元のデータソースへのアクセス回数を減らし、全体的なパフォーマンスを向上させる技術です。
sync.Once
: Go言語のsync
パッケージが提供するプリミティブの一つで、特定の関数が複数回呼び出されても、その関数が一度だけ実行されることを保証します。これは、遅延初期化(lazy initialization)や、リソースの単一インスタンス生成などに非常に有用です。sync.RWMutex
: Go言語のsync
パッケージが提供する読み書きロック(Reader-Writer Mutex)です。複数のゴルーチンが同時にデータを読み取ることは許可しますが、書き込みは一度に一つのゴルーチンのみに制限します。これにより、読み取りが頻繁で書き込みが少ないデータ構造において、高い並行性を維持しつつデータの一貫性を保証できます。
技術的詳細
このコミットの技術的な核心は、Plan 9の環境変数をメモリ上にキャッシュし、そのキャッシュへのアクセスを並行性セーフにするためのGo言語の並行性プリミティブの活用です。
- 環境変数キャッシュの導入:
env map[string]string
というグローバルマップが導入され、環境変数のキーと値をメモリ上に保持します。
- 遅延初期化と単一実行の保証:
envOnce sync.Once
が導入され、copyenv()
関数が一度だけ実行されることを保証します。copyenv()
は、/env
ディレクトリを読み込み、全ての環境変数をenv
マップにロードする役割を担います。これにより、環境変数のキャッシュは、最初にGetenv
、Setenv
、Clearenv
、またはEnviron
が呼び出されたときに一度だけ初期化されます。
- 並行性セーフなアクセス:
envLock sync.RWMutex
が導入され、env
マップへの並行アクセスを保護します。Getenv
およびEnviron
(読み取り操作)では、envLock.RLock()
とenvLock.RUnlock()
を使用して読み取りロックを取得・解放します。これにより、複数のゴルーチンが同時に環境変数を読み取ることができます。Setenv
およびClearenv
(書き込み操作)では、envLock.Lock()
とenvLock.Unlock()
を使用して書き込みロックを取得・解放します。これにより、書き込み中は他の読み取りや書き込みがブロックされ、データの一貫性が保たれます。
- システムコールとキャッシュの連携:
readenv(key string) (string, error)
: これは、元のGetenv
のファイルシステム読み取りロジックを抽出したヘルパー関数です。キャッシュミス時やcopyenv
の初期化時にのみ使用されます。writeenv(key, value string) error
: これは、元のSetenv
のファイルシステム書き込みロジックを抽出したヘルパー関数です。Setenv
が呼び出された際に、実際のPlan 9環境変数ファイルに書き込むために使用されます。Setenv
は、writeenv
を呼び出して実際の環境変数を更新した後、キャッシュ(env
マップ)も更新します。Clearenv
は、キャッシュ(env
マップ)をクリアするとともに、RawSyscall(SYS_RFORK, RFCENVG, 0, 0)
を呼び出して実際のPlan 9環境もクリアします。
この設計により、環境変数の読み取りはほとんどの場合、高速なメモリキャッシュから行われるようになり、システムコールによるオーバーヘッドが大幅に削減されます。書き込み操作は依然としてシステムコールを伴いますが、読み取り操作に比べて頻度が低いことが多いため、全体的なパフォーマンス向上に寄与します。
コアとなるコードの変更箇所
変更はsrc/pkg/syscall/env_plan9.go
ファイルに集中しています。
--- a/src/pkg/syscall/env_plan9.go
+++ b/src/pkg/syscall/env_plan9.go
@@ -6,69 +6,123 @@
package syscall
-import "errors"
+import (
+ "errors"
+ "sync"
+)
-func Getenv(key string) (value string, found bool) {
-- if len(key) == 0 {
-- return "", false
- }
-- f, e := Open("/env/"+key, O_RDONLY)
-- if e != nil {
-- return "", false
- }
-- defer Close(f)
-
-- l, _ := Seek(f, 0, 2)
-- Seek(f, 0, 0)
-- buf := make([]byte, l)
-- n, e := Read(f, buf)
-- if e != nil {
- return "", false
- }
-
-- if n > 0 && buf[n-1] == 0 {
-- buf = buf[:n-1]
- }
-- return string(buf), true
-}
+var (
+ // envOnce guards initialization by 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
+)
+
+func readenv(key string) (string, error) {
+ fd, err := Open("/env/"+key, O_RDONLY)
+ if err != nil {
+ return "", err
+ }
+ defer Close(fd)
+ l, _ := Seek(fd, 0, 2)
+ Seek(fd, 0, 0)
+ buf := make([]byte, l)
+ n, err := Read(fd, buf)
+ if err != nil {
+ return "", err
+ }
+ if n > 0 && buf[n-1] == 0 {
+ buf = buf[:n-1]
+ }
+ return string(buf), nil
+}
func Setenv(key, value string) error {
+ fd, err := Create("/env/"+key, O_RDWR, 0666)
+ if err != nil {
+ return err
+ }
+ defer Close(fd)
+ _, err = Write(fd, []byte(value))
+ return err
+}
+
+func copyenv() {
+ env = make(map[string]string)
+ fd, err := Open("/env", O_RDONLY)
+ if err != nil {
+ return
+ }
+ defer Close(fd)
+ files, err := readdirnames(fd)
+ if err != nil {
+ return
+ }
+ for _, key := range files {
+ v, err := readenv(key)
+ if err != nil {
+ continue
+ }
+ env[key] = v
+ }
+}
+
+func Getenv(key string) (value string, found bool) {
+ envOnce.Do(copyenv)
+ if len(key) == 0 {
+ return "", false
+ }
+
+ envLock.RLock()
+ defer envLock.RUnlock()
+
+ v, ok := env[key]
+ if !ok {
+ return "", false
+ }
+ return v, true
+}
+
+func Setenv(key, value string) error {
+ envOnce.Do(copyenv)
if len(key) == 0 {
- return errors.New("bad arg in system call")
+ return errors.New("zero length key")
}
- f, e := Create("/env/"+key, O_RDWR, 0666)
- if e != nil {
- return e
- }
- defer Close(f)
-
-- _, e = Write(f, []byte(value))
+ envLock.Lock()
+ defer envLock.Unlock()
+
+ err := writeenv(key, value)
+ if err != nil {
+ return err
+ }
+ env[key] = value
return nil
}
func Clearenv() {
+ envOnce.Do(copyenv) // prevent copyenv in Getenv/Setenv
+
+ envLock.Lock()
+ defer envLock.Unlock()
+
+ env = make(map[string]string)
RawSyscall(SYS_RFORK, RFCENVG, 0, 0)
}
func Environ() []string {
- env := make([]string, 0, 100)
-
-- f, e := Open("/env", O_RDONLY)
-- if e != nil {
-- panic(e)
-- }
-- defer Close(f)
--
-- names, e := readdirnames(f)
-- if e != nil {
-- panic(e)
-- }
--
-- for _, k := range names {
-- if v, ok := Getenv(k); ok {
-- env = append(env, k+"="+v)
-- }
- }
-- return env[0:len(env)]
+ envOnce.Do(copyenv)
+ envLock.RLock()
+ defer envLock.RUnlock()
+ a := make([]string, len(env))
+ i := 0
+ for k, v := range env {
+ a[i] = k + "=" + v
+ i++
+ }
+ return a
}
コアとなるコードの解説
import ("errors", "sync")
:sync
パッケージが新しくインポートされ、sync.Once
とsync.RWMutex
が使用可能になります。var (envOnce sync.Once, envLock sync.RWMutex, env map[string]string)
:envOnce
:copyenv
関数の初回実行を保証するためのsync.Once
インスタンス。envLock
:env
マップへの並行アクセスを制御するための読み書きロック。env
: 環境変数をキャッシュするためのstring
からstring
へのマップ。
readenv(key string) (string, error)
:- 元の
Getenv
関数から、単一の環境変数ファイルを読み取るロジックが抽出されました。これは、Plan 9の/env/key
ファイルを開き、その内容を読み取って文字列として返す関数です。
- 元の
writeenv(key, value string) error
:- 元の
Setenv
関数から、単一の環境変数ファイルに書き込むロジックが抽出されました。これは、Plan 9の/env/key
ファイルを作成または開き、指定された値を書き込む関数です。
- 元の
copyenv()
:- この新しい関数は、
/env
ディレクトリ内の全ての環境変数ファイルを読み込み、その内容をenv
マップに格納します。これは、キャッシュの初期化処理です。
- この新しい関数は、
Getenv(key string) (value string, found bool)
:envOnce.Do(copyenv)
:Getenv
が最初に呼び出されたときにcopyenv
が一度だけ実行され、キャッシュが初期化されます。envLock.RLock()
/defer envLock.RUnlock()
:env
マップからの読み取り操作中に読み取りロックを取得し、関数終了時に解放します。これにより、複数の読み取りが同時に行われても安全です。v, ok := env[key]
: キャッシュから直接環境変数の値を取得しようとします。キャッシュに存在すれば、ファイルシステムアクセスなしで値を返します。
Setenv(key, value string) error
:envOnce.Do(copyenv)
:Setenv
が最初に呼び出されたときもキャッシュが初期化されます。envLock.Lock()
/defer envLock.Unlock()
:env
マップへの書き込み操作中に書き込みロックを取得し、関数終了時に解放します。これにより、書き込み中は他の読み取りや書き込みがブロックされ、データの一貫性が保証されます。err := writeenv(key, value)
: 実際のPlan 9環境変数ファイルに値を書き込みます。env[key] = value
: ファイルへの書き込みが成功した後、キャッシュも更新します。
Clearenv()
:envOnce.Do(copyenv)
:Clearenv
が呼び出された後でも、Getenv
/Setenv
がcopyenv
を再度実行しないようにします。envLock.Lock()
/defer envLock.Unlock()
: キャッシュをクリアする前に書き込みロックを取得します。env = make(map[string]string)
: キャッシュを空のマップで上書きし、クリアします。RawSyscall(SYS_RFORK, RFCENVG, 0, 0)
: 実際のPlan 9環境をクリアするためのシステムコールを呼び出します。
Environ() []string
:envOnce.Do(copyenv)
: キャッシュを初期化します。envLock.RLock()
/defer envLock.RUnlock()
: キャッシュからの読み取り操作中に読み取りロックを取得します。for k, v := range env
: キャッシュされたenv
マップをイテレートし、key=value
形式の文字列スライスを構築して返します。以前のように環境変数ファイルを一つずつ読み込む必要がなくなりました。
これらの変更により、Plan 9環境におけるGoプログラムの環境変数アクセス性能が大幅に向上し、特に環境変数を頻繁に参照するアプリケーションにおいてその効果が顕著に現れます。
関連リンク
- Go言語の
sync
パッケージドキュメント: https://pkg.go.dev/sync - Go言語の
syscall
パッケージドキュメント: https://pkg.go.dev/syscall - Plan 9 from Bell Labs: https://9p.io/plan9/
参考にした情報源リンク
- Go言語の公式ドキュメント
- Plan 9の環境変数に関する情報(
/env
ファイルシステム) - Go言語の並行性プリミティブ(
sync.Once
,sync.RWMutex
)に関する一般的な解説 - システムコールとパフォーマンスに関する一般的な知識