Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

[インデックス 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マップの初期化と、GetenvSetenvClearenvEnvironといった環境変数関連の関数におけるキャッシュの利用方法です。

変更前は、GetenvSetenvClearenvのいずれかが最初に呼び出された際に、sync.Onceを用いてcopyenv関数が実行され、システム上の全ての環境変数が一度に読み込まれてenvマップにキャッシュされていました。

変更の背景

この変更の背景には、パフォーマンスの最適化があります。従来のGoのsyscallパッケージにおけるPlan 9環境での環境変数管理では、Getenv(単一の環境変数を取得する関数)が初めて呼び出された際に、システム上の全ての環境変数を読み込み、内部キャッシュ(envマップ)に格納していました。これは、copyenv関数がsync.Onceによって一度だけ実行されることで実現されていました。

しかし、このアプローチには以下の問題がありました。

  1. 不要なシステムコール: Getenvが呼び出された際に、実際に必要とされるのは特定の1つの環境変数だけであるにもかかわらず、全ての環境変数を読み込むために多くのシステムコールが発生していました。これは、特に環境変数の数が多い場合にオーバーヘッドとなります。
  2. 遅延の発生: 初めて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キャッシュの管理方法の変更です。

  1. envマップの初期化: 変更前はcopyenv関数内でenv = make(map[string]string)としていましたが、変更後はvar env = make(map[string]string)としてグローバル変数宣言時に初期化されるようになりました。これにより、マップ自体は常に利用可能な状態になります。

  2. 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の初回呼び出し時でも、必要な環境変数のみがシステムコールによって読み込まれ、キャッシュに追加されます。
  3. 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が実行され、全ての環境変数がキャッシュに読み込まれるという、より効率的な動作が実現されます。

  4. エラー変数の導入: zero length keyshort writeのエラーメッセージが、それぞれerrZeroLengthKeyerrShortWriteという変数として定義されました。これにより、エラーメッセージの再利用性と一貫性が向上します。

  5. 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である可能性がなくなります。また、新しいエラー変数errZeroLengthKeyerrShortWriteが導入され、エラーハンドリングの統一性が図られています。

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ファイルシステムについて)

参考にした情報源リンク