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

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

このコミットは、Goコマンド(cmd/go)が外部コマンドを実行する際に、そのコマンドの環境変数に$PWD(現在の作業ディレクトリ)を設定する変更を導入しています。これにより、実行される外部コマンド内でos.Getwd関数が呼び出された際のパフォーマンスが大幅に向上します。

コミット

commit 357a18a2c625d06f0f9202142b3432458368484f
Author: Russ Cox <rsc@golang.org>
Date:   Thu Feb 14 14:21:44 2013 -0500

    cmd/go: set $PWD when running commands
    
    This makes os.Getwd inside those commands much faster.
    
    R=golang-dev, r
    CC=golang-dev
    https://golang.org/cl/7324055

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

https://github.com/golang/go/commit/357a18a2c625d06f0f9202142b3432458368484f

元コミット内容

cmd/go: コマンド実行時に$PWDを設定する。 これにより、それらのコマンド内でのos.Getwdがはるかに高速になる。

変更の背景

Goのビルドシステムやテスト実行、VCS(バージョン管理システム)操作など、cmd/goが内部的に外部コマンドを呼び出す際、その外部コマンドが自身の現在の作業ディレクトリ(CWD)を取得するためにos.Getwdのようなシステムコールを使用することがあります。

従来のGoコマンドの実装では、外部コマンドを実行する際に、そのコマンドの環境変数に明示的に$PWDを設定していませんでした。os.Getwdは、通常、現在の作業ディレクトリを決定するためにシステムコール(例: Linuxのgetcwd)を発行します。このシステムコールは、ファイルシステムを走査する必要があるため、特に深いディレクトリ階層やネットワークファイルシステム上では、比較的コストの高い操作となる可能性があります。

このコミットの背景には、os.Getwdの呼び出しが頻繁に行われるシナリオにおいて、そのパフォーマンスを改善したいという意図があります。$PWD環境変数を設定することで、多くのシステムではos.Getwdがシステムコールを発行する代わりに、この環境変数の値を利用して現在の作業ディレクトリを迅速に取得できるようになります。これにより、Goコマンドが起動する外部プロセス全体の効率が向上します。

前提知識の解説

os.Getwd関数

Go言語の標準ライブラリosパッケージに含まれるGetwd()関数は、現在の作業ディレクトリ(Current Working Directory, CWD)の絶対パスを返します。この関数は、プログラムがファイルシステム上のどこで実行されているかを知るために広く使用されます。内部的には、オペレーティングシステムが提供するAPI(例: POSIXシステムにおけるgetcwd())を呼び出してCWDを取得します。

環境変数 $PWD

PWDは、多くのUnix系オペレーティングシステムで使用される標準的な環境変数です。この変数は、現在のシェルセッションの作業ディレクトリの絶対パスを保持します。シェルは、cdコマンドなどでディレクトリを変更するたびに、このPWD変数を自動的に更新します。

多くのプログラムやライブラリは、os.GetwdのようなCWD取得関数を呼び出す際に、まず$PWD環境変数の値をチェックします。もし$PWDが設定されており、その値が有効なディレクトリを指していれば、システムコールを発行するよりも高速にCWDを取得できる場合があります。これは、システムコールがカーネルモードへの切り替えやファイルシステムのスキャンを伴うのに対し、環境変数の読み取りはユーザーモードで完結するためです。

cmd/goの動作と外部コマンドの実行

cmd/goは、Go言語のビルド、テスト、依存関係管理などを行うための主要なコマンドラインツールです。このツールは、その機能の一部として、コンパイラ(go tool compile)、リンカ(go tool link)、テストバイナリ、バージョン管理システム(Git, Mercurialなど)のクライアントなど、様々な外部コマンドを起動します。

Go言語では、os/execパッケージを使用して外部コマンドを実行します。exec.Command構造体には、実行するコマンド、引数、作業ディレクトリ(Dirフィールド)、環境変数(Envフィールド)などを設定できます。このコミット以前は、cmd/goが外部コマンドを起動する際に、Envフィールドに$PWDを明示的に設定していませんでした。

技術的詳細

このコミットの核心は、cmd/goが外部コマンドを実行する際に、そのコマンドの環境変数リストに現在の作業ディレクトリを示すPWD変数を追加することです。

具体的には、src/cmd/go/main.goenvForDirという新しいヘルパー関数が導入されました。この関数の目的は、指定されたディレクトリdirに対して適切な環境変数のリストを生成することです。

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

  1. まず、現在のプロセスの環境変数リスト(os.Environ())を取得します。
  2. このリストを走査し、既存のPWD=で始まるエントリを探します。
  3. もしPWD=エントリが見つかった場合、その値を引数で渡されたdirのパスに更新します。
  4. PWD=エントリが見つからなかった場合、新しいPWD=エントリを"PWD=" + dirとして環境変数リストの末尾に追加します。
  5. 更新または追加された環境変数リストを返します。

このenvForDir関数は、cmd/goが外部コマンドを実行する様々な箇所で利用されるようになりました。具体的には、以下のファイルでcmd.Env = envForDir(cmd.Dir)という形で適用されています。

  • src/cmd/go/build.go: ビルドプロセス中に外部ツール(コンパイラ、リンカなど)を実行するbuilder.runOutメソッド内で使用されます。
  • src/cmd/go/test.go: テストバイナリを実行するbuilder.runTestメソッド内で使用されます。
  • src/cmd/go/vcs.go: バージョン管理システム(Gitなど)のコマンドを実行するvcsCmd.run1メソッド内で使用されます。

これにより、cmd/goによって起動されるすべての外部プロセスは、その実行環境に正しい$PWD環境変数を持つことになります。結果として、これらの外部プロセス内でos.Getwdが呼び出された際に、システムコールを介さずに$PWDから直接現在の作業ディレクトリを取得できる可能性が高まり、パフォーマンスが向上します。

この変更は、特にGoのビルドやテストが頻繁に実行されるCI/CD環境や、大規模なGoプロジェクトにおいて、ビルド時間の短縮に貢献する可能性があります。

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

このコミットでは、主に以下の4つのファイルが変更されています。

  1. src/cmd/go/build.go
  2. src/cmd/go/main.go
  3. src/cmd/go/test.go
  4. src/cmd/go/vcs.go

src/cmd/go/build.go

--- a/src/cmd/go/build.go
+++ b/src/cmd/go/build.go
@@ -1226,7 +1226,7 @@ func (b *builder) runOut(dir string, desc string, cmdargs ...interface{}) ([]byt
 		cmd.Stdout = &buf
 		cmd.Stderr = &buf
 		cmd.Dir = dir
-		// TODO: cmd.Env
+		cmd.Env = envForDir(cmd.Dir)
 		err := cmd.Run()
 
 		// cmd.Run will fail on Unix if some other process has the binary

builder.runOut関数内で、cmd.EnvenvForDir(cmd.Dir)を設定する行が追加されました。これにより、ビルド関連の外部コマンド実行時にPWDが設定されます。

src/cmd/go/main.go

--- a/src/cmd/go/main.go
+++ b/src/cmd/go/main.go
@@ -379,6 +379,25 @@ func runOut(dir string, cmdargs ...interface{}) []byte {
 	return out
 }
 
+// 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
+		}
+	}
+	// Internally we only use rooted paths, so dir is rooted.
+	// Even if dir is not rooted, no harm done.
+	env = append(env, "PWD="+dir)
+	return env
+}
+
 // matchPattern(pattern)(name) reports whether
 // name matches pattern.  Pattern is a limited glob
 // pattern in which '...' means 'any string' and there

envForDirという新しいヘルパー関数が追加されました。この関数は、指定されたディレクトリdirに基づいてPWD環境変数を適切に設定した環境変数リストを返します。

src/cmd/go/test.go

--- a/src/cmd/go/test.go
+++ b/src/cmd/go/test.go
@@ -635,6 +635,7 @@ func (b *builder) runTest(a *action) error {
 
 	cmd := exec.Command(args[0], args[1:]...)
 	cmd.Dir = a.p.Dir
+	cmd.Env = envForDir(cmd.Dir)
 	var buf bytes.Buffer
 	if testStreamOutput {
 		cmd.Stdout = os.Stdout
@@ -647,7 +648,7 @@ func (b *builder) runTest(a *action) error {
 	// If there are any local SWIG dependencies, we want to load
 	// the shared library from the build directory.
 	if a.p.usesSwig() {
-		env := os.Environ()
+		env := cmd.Env
 		found := false
 		prefix := "LD_LIBRARY_PATH="
 		for i, v := range env {

builder.runTest関数内で、cmd.EnvenvForDir(cmd.Dir)を設定する行が追加されました。また、SWIG関連の環境変数設定箇所で、既存のos.Environ()の代わりにcmd.Env(すでにPWDが設定されているもの)を使用するように変更されています。

src/cmd/go/vcs.go

--- a/src/cmd/go/vcs.go
+++ b/src/cmd/go/vcs.go
@@ -190,6 +190,7 @@ func (v *vcsCmd) run1(dir string, cmdline string, keyval []string, verbose bool)\
 
 	cmd := exec.Command(v.cmd, args...)
 	cmd.Dir = dir
+	cmd.Env = envForDir(cmd.Dir)
 	if buildX {
 		fmt.Printf("cd %s\\n", dir)
 		fmt.Printf("%s %s\\n", v.cmd, strings.Join(args, " "))

vcsCmd.run1関数内で、cmd.EnvenvForDir(cmd.Dir)を設定する行が追加されました。これにより、VCSコマンド実行時にもPWDが設定されます。

コアとなるコードの解説

このコミットの最も重要な部分は、src/cmd/go/main.goに追加された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=") { // 既存のPWDエントリを探す
			env[i] = "PWD=" + dir // 見つかったら更新
			return env
		}
	}
	// Internally we only use rooted paths, so dir is rooted.
	// Even if dir is not rooted, no harm done.
	env = append(env, "PWD="+dir) // 見つからなかったら追加
	return env
}

この関数は、Goコマンドが外部プロセスを起動する際に、そのプロセスに渡す環境変数リストを生成します。

  1. os.Environ()を呼び出すことで、現在のGoコマンド自身の環境変数リストのコピーを取得します。
  2. ループを使って、このリストの中に"PWD="で始まるエントリがあるかどうかを確認します。
  3. もし"PWD="エントリが見つかった場合、そのエントリの値を、外部プロセスが実行されるべきディレクトリdirのパスに更新します。そして、更新された環境変数リストをすぐに返します。これにより、既存のPWD変数を上書きし、重複を防ぎます。
  4. もし"PWD="エントリが見つからなかった場合、新しい"PWD=" + dirという形式のエントリを環境変数リストの末尾に追加します。
  5. 最終的に、PWDが適切に設定された環境変数リストが返されます。

このenvForDir関数が、src/cmd/go/build.gobuilder.runOutsrc/cmd/go/test.gobuilder.runTest、そしてsrc/cmd/go/vcs.govcsCmd.run1といった、外部コマンドを実行する主要な箇所でcmd.Envフィールドに割り当てられることで、Goコマンドが起動するすべての外部プロセスが、その作業ディレクトリに対応する$PWD環境変数を持つようになります。

これにより、外部プロセス内でos.Getwdが呼び出された際に、多くのシステムでは$PWD環境変数を参照して高速に現在の作業ディレクトリを取得できるようになり、システムコールによるオーバーヘッドが削減され、全体的なパフォーマンスが向上します。

関連リンク

参考にした情報源リンク