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

[インデックス 15242] ファイルの概要

このコミットは、Go言語のテスト実行環境におけるパフォーマンス改善を目的としています。具体的には、os.Getwd(現在のワーキングディレクトリを取得する関数)の呼び出しコストを削減するために、子プロセスに渡される環境変数$PWDを適切に設定する変更が加えられています。

コミット

commit 551f3f27aaa69495a499eb3637b79480a2054cbc
Author: Russ Cox <rsc@golang.org>
Date:   Thu Feb 14 14:21:26 2013 -0500

    test/run: use correct $PWD to make os.Getwd less expensive

    The commands being run are 'go tool this' and 'go tool that',
    and the go command will call Getwd during its init.

    R=golang-dev, iant
    CC=golang-dev
    https://golang.org/cl/7336045

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/551f3f27aaa69495a499eb3637b79480a2054cbc

元コミット内容

test/run: use correct $PWD to make os.Getwd less expensive

The commands being run are 'go tool this' and 'go tool that',
and the go command will call Getwd during its init.

変更の背景

Go言語のテストスイートでは、go toolコマンドを使用して様々なテストが実行されます。これらのgo toolコマンドは、起動時にos.Getwd関数を呼び出して現在のワーキングディレクトリを取得します。

os.Getwdは、システムコールを通じて現在のワーキングディレクトリを問い合わせるため、特に頻繁に呼び出される場合や、ファイルシステムの状態によっては、比較的コストの高い操作となる可能性があります。

このコミットの背景には、テスト実行時のos.Getwdの呼び出しがパフォーマンスボトルネックになっているという認識がありました。子プロセス(go toolコマンド)が起動される際に、親プロセスが既に知っているワーキングディレクトリ情報を環境変数$PWDとして渡すことで、子プロセスがos.Getwdを呼び出す際のオーバーヘッドを削減し、テスト全体の実行速度を向上させることが目的でした。

前提知識の解説

1. os.Getwd関数 (Go言語)

os.Getwd()は、Go言語の標準ライブラリosパッケージに含まれる関数で、現在のワーキングディレクトリの絶対パスを文字列として返します。内部的には、オペレーティングシステムが提供するシステムコール(例: Linux/Unix系ではgetcwd(2)、WindowsではGetCurrentDirectory)を呼び出して情報を取得します。

このシステムコールは、ファイルシステムを走査してパスを解決する必要があるため、特に深いディレクトリ階層やネットワークファイルシステム上では、ある程度の時間がかかることがあります。

2. $PWD 環境変数

$PWDは、多くのUnix系シェル(bash, zshなど)で使われる環境変数で、"Print Working Directory"の略です。この変数は、現在のシェルのワーキングディレクトリの絶対パスを保持しています。

シェルは、cdコマンドなどでディレクトリを移動するたびに、この$PWD変数を自動的に更新します。これにより、ユーザーはpwdコマンドを実行する代わりにecho $PWDで現在のディレクトリを素早く確認できます。

プログラムが子プロセスを起動する際、親プロセスの環境変数は通常、子プロセスに継承されます。子プロセスは、この$PWD環境変数を参照することで、システムコールを介さずに自身のワーキングディレクトリを効率的に知ることができます。多くのプログラムやライブラリは、パフォーマンス最適化のために、まず$PWD環境変数をチェックし、それが存在しないか不正な場合にのみos.Getwdのようなシステムコールにフォールバックする実装を採用しています。

3. os/execパッケージとcmd.Dir, cmd.Env

Go言語で外部コマンドを実行するには、os/execパッケージを使用します。

  • exec.Command関数でCmd構造体を作成し、実行するコマンドと引数を指定します。
  • Cmd.Dirフィールドは、コマンドを実行するワーキングディレクトリを指定します。このフィールドが設定されていない場合、コマンドは親プロセスと同じワーキングディレクトリで実行されます。
  • Cmd.Envフィールドは、子プロセスに渡す環境変数を[]string型で指定します。各要素はKEY=VALUE形式の文字列です。このフィールドがnilの場合、子プロセスは親プロセスの環境変数をそのまま継承します。Cmd.Envが設定されている場合、親プロセスの環境変数は継承されず、指定された環境変数のみが子プロセスに渡されます。

技術的詳細

このコミットは、test/run.goファイル内のtest構造体のrunメソッドに修正を加えています。runメソッドは、Goのテストスイートが外部コマンド(特にgo toolコマンド)を実行する際に使用されます。

変更の核心は、cmd.Dirが設定される(つまり、一時ディレクトリでコマンドが実行される)場合に、cmd.Envも明示的に設定し、その中に正しい$PWD環境変数を含めるようにした点です。

具体的には、以下の新しい関数envForDirが追加されました。

// envForDir returns a copy of the environment
// suitable for running in the given directory.
// The environment is the current process's environment
// but with an updated $PWD, so that an os.Getwd in the
// child will be faster.
func envForDir(dir string) []string {
	env := os.Environ() // 現在のプロセスの環境変数を取得
	for i, kv := range env {
		if strings.HasPrefix(kv, "PWD=") {
			env[i] = "PWD=" + dir // 既存のPWDがあれば更新
			return env
		}
	}
	env = append(env, "PWD="+dir) // PWDがなければ追加
	return env
}

このenvForDir関数は、以下のロジックで動作します。

  1. os.Environ()を呼び出して、現在のプロセスのすべての環境変数を取得します。
  2. 取得した環境変数リストをループし、PWD=で始まるエントリを探します。
  3. もしPWD=で始まるエントリが見つかった場合、そのエントリの値を引数dirで渡された新しいディレクトリパスに更新し、更新された環境変数リストを返します。
  4. PWD=で始まるエントリが見つからなかった場合、新しいPWD=エントリを環境変数リストの末尾に追加し、そのリストを返します。

この関数が返す環境変数リストは、cmd.Envに設定されます。これにより、子プロセスは、os.Getwdを呼び出す前に、まず環境変数$PWDから現在のワーキングディレクトリを効率的に取得できるようになります。これにより、システムコールを回避できるため、パフォーマンスが向上します。

コアとなるコードの変更箇所

test/run.goファイルに以下の変更が加えられました。

  1. func (t *test) run() メソッド内のif useTmpブロックに、cmd.Env = envForDir(cmd.Dir)の行が追加されました。
  2. 新しいヘルパー関数func envForDir(dir string) []stringがファイル末尾に追加されました。
--- a/test/run.go
+++ b/test/run.go
@@ -433,6 +433,7 @@ func (t *test) run() {
 		cmd.Stderr = &buf
 		if useTmp {
 			cmd.Dir = t.tempDir
+			cmd.Env = envForDir(cmd.Dir)
 		}
 		err := cmd.Run()
 		if err != nil {
@@ -828,3 +829,20 @@ func checkShouldTest() {
 	assertNot(shouldTest("// +build arm 386", "linux", "amd64"))
 	assert(shouldTest("// This is a test.", "os", "arch"))
 }
+
+// envForDir returns a copy of the environment
+// suitable for running in the given directory.
+// The environment is the current process's environment
+// but with an updated $PWD, so that an os.Getwd in the
+// child will be faster.
+func envForDir(dir string) []string {
+	env := os.Environ()
+	for i, kv := range env {
+		if strings.HasPrefix(kv, "PWD=") {
+			env[i] = "PWD=" + dir
+			return env
+		}
+	}
+	env = append(env, "PWD="+dir)
+	return env
+}

コアとなるコードの解説

func (t *test) run() 内の変更

		if useTmp {
			cmd.Dir = t.tempDir
			cmd.Env = envForDir(cmd.Dir) // 追加された行
		}

この部分では、テストが一時ディレクトリ(t.tempDir)で実行される場合に、子プロセスのワーキングディレクトリをt.tempDirに設定しています。そして、追加された行cmd.Env = envForDir(cmd.Dir)によって、この一時ディレクトリのパスを$PWD環境変数として子プロセスに明示的に渡すようにしています。これにより、子プロセスがos.Getwdを呼び出す際に、環境変数から直接パスを取得できる可能性が高まり、システムコールによるオーバーヘッドを回避できます。

func envForDir(dir string) []string 関数

この関数は、子プロセスに渡すための環境変数リストを生成します。

  1. env := os.Environ(): 現在のGoプロセスの環境変数(PATH, HOMEなど、親シェルから継承されたもの全て)のコピーを取得します。
  2. for i, kv := range env: 取得した環境変数リストを反復処理します。kvKEY=VALUE形式の文字列です。
  3. if strings.HasPrefix(kv, "PWD=") : 各環境変数がPWD=で始まるかどうかをチェックします。これは、既存の$PWD環境変数を見つけるためです。
  4. env[i] = "PWD=" + dir: もし既存の$PWDが見つかった場合、その値を新しいディレクトリパスdirで上書きします。
  5. return env: 更新された環境変数リストを即座に返します。これにより、リストの残りの部分を不必要に処理することを避けます。
  6. env = append(env, "PWD="+dir): ループが終了してもPWDが見つからなかった場合(つまり、現在のプロセスに$PWDが設定されていなかった場合)、新しいPWD=エントリをリストの末尾に追加します。
  7. return env: 最終的な環境変数リストを返します。

この関数は、既存の$PWDを更新するか、存在しない場合は新しく追加することで、子プロセスが常に正しいワーキングディレクトリ情報を$PWD経由で利用できるようにします。

関連リンク

参考にした情報源リンク

  • Go CL 7336045: https://golang.org/cl/7336045 (コミットメッセージに記載されているGoのコードレビューシステムへのリンク)
  • Go言語の公式ドキュメント
  • Unix/Linuxのシェルおよび環境変数に関する一般的な知識
  • Go言語のos/execパッケージの動作に関する一般的な知識