[インデックス 15247] ファイルの概要
このコミットは、Go言語のgo/types
パッケージにおけるos.Getwd()
関数の呼び出しを最適化し、不要な場合にこの高コストな操作を避けるように変更するものです。具体的には、ローカルインポートではない場合にはos.Getwd()
の呼び出しをスキップすることで、パフォーマンスの改善を図っています。
コミット
commit da6207f7a48a0e0bb961e0d80ca454ebd02180da
Author: Russ Cox <rsc@golang.org>
Date: Thu Feb 14 14:46:03 2013 -0500
go/types: avoid os.Getwd if not necessary
Getwd can be very expensive.
R=golang-dev, r
CC=golang-dev
https://golang.org/cl/7312100
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/da6207f7a48a0e0bb961e0d80ca454ebd02180da
元コミット内容
go/types: avoid os.Getwd if not necessary
Getwd can be very expensive.
R=golang-dev, r
CC=golang-dev
https://golang.org/cl/7312100
変更の背景
この変更の背景には、os.Getwd()
(現在の作業ディレクトリを取得する関数)の呼び出しが、特に一部の環境やファイルシステムにおいて非常に高コストな操作となる可能性があるという認識があります。Goコンパイラやツールチェーンがパッケージのインポートパスを解決する際、go/types
パッケージはインポートされたパッケージの場所を特定するために現在の作業ディレクトリを必要とすることがあります。しかし、全てのインポートが現在の作業ディレクトリからの相対パスであるわけではありません。例えば、標準ライブラリのパッケージや、GOPATH/GOROOT配下にインストールされたパッケージなど、絶対パスや既知の場所から解決できるインポートの場合、os.Getwd()
を呼び出す必要はありません。
このコミットは、このような不要なos.Getwd()
の呼び出しを特定し、スキップすることで、コンパイル時間やツールチェーンのパフォーマンスを改善することを目的としています。特に、大規模なプロジェクトや、ネットワークファイルシステムを使用している環境では、この最適化が顕著な効果をもたらす可能性があります。
前提知識の解説
1. os.Getwd()
関数
os.Getwd()
はGo言語のos
パッケージに含まれる関数で、現在の作業ディレクトリ(Current Working Directory, CWD)の絶対パスを返します。この関数は、システムコールを通じてOSから現在の作業ディレクトリの情報を取得します。
パフォーマンスに関する考慮事項:
- システムコール:
os.Getwd()
はシステムコールを伴うため、純粋なCPU計算に比べてオーバーヘッドがあります。 - ファイルシステムの種類: ネットワークファイルシステム(NFS, SMBなど)や、非常に深いディレクトリ構造を持つファイルシステムでは、現在の作業ディレクトリのパスを解決するのに時間がかかることがあります。これは、パスの各コンポーネントを解決するために複数のディレクトリ参照が必要になるためです。
- 頻繁な呼び出し: プログラム内で
os.Getwd()
が頻繁に呼び出されると、その累積コストがパフォーマンスボトルネックとなる可能性があります。
2. Goのパッケージインポート
Go言語では、import
ステートメントを使用して他のパッケージの機能を利用します。インポートパスは、Goツールチェーンがパッケージを見つけるための手がかりとなります。
- 標準ライブラリ:
fmt
,net/http
などの標準ライブラリは、Goのインストールディレクトリ(GOROOT)配下にあります。 - 外部パッケージ:
go get
などで取得した外部パッケージは、GOPATH配下にインストールされます。 - ローカルインポート: 同じプロジェクト内の他のパッケージをインポートする場合、相対パス(例:
./mypackage
)や、モジュールパスからの相対パスを使用することがあります。
Goツールチェーンは、これらのインポートパスを解決するために、GOROOT、GOPATH、そして現在の作業ディレクトリなどを参照します。
3. go/types
パッケージ
go/types
パッケージは、Go言語の型システムを扱うためのライブラリです。Goコンパイラや、Goのコードを解析するツール(例えば、GoLandのようなIDEやgo vet
のような静的解析ツール)で利用されます。このパッケージは、ソースコードを解析し、型チェックを行い、パッケージ間の依存関係を解決する役割を担います。パッケージのインポート解決もその機能の一部です。
4. go/build
パッケージとbuild.IsLocalImport()
go/build
パッケージは、Goのビルドシステムに関する情報を提供します。これには、パッケージの検索パス、ビルドタグ、そしてインポートパスの種類を識別する機能などが含まれます。
build.IsLocalImport(path string)
関数は、与えられたインポートパスがローカルインポート(つまり、現在の作業ディレクトリからの相対パスとして解決される可能性のあるインポート)であるかどうかを判定します。例えば、./mypackage
のようなパスはローカルインポートと判定されますが、fmt
やgithub.com/some/repo
のようなパスはローカルインポートではありません。
技術的詳細
このコミットの技術的な核心は、go/types
パッケージのGcImport
関数におけるos.Getwd()
の呼び出しを条件付きにすることです。
変更前のコードでは、GcImport
関数が呼び出されると、まず無条件にos.Getwd()
を呼び出して現在の作業ディレクトリを取得していました。このsrcDir
は、その後のFindPkg
関数でパッケージの検索パスとして利用されます。
変更後のコードでは、srcDir
の初期値を"."
(カレントディレクトリを示す)とし、build.IsLocalImport(path)
のチェックを追加しています。
// 変更前
srcDir, err := os.Getwd()
if err != nil {
return
}
// 変更後
srcDir := "."
if build.IsLocalImport(path) {
srcDir, err = os.Getwd()
if err != nil {
return
}
}
この変更により、以下のロジックが適用されます。
-
path
がローカルインポートではない場合:build.IsLocalImport(path)
がfalse
を返します。この場合、os.Getwd()
は呼び出されず、srcDir
は初期値の"."
のままとなります。FindPkg
関数は、この"."
を基点としてパッケージを検索しますが、ローカルインポートではないため、通常はGOPATHやGOROOTなどの標準的な検索パスで解決されます。os.Getwd()
の呼び出しコストが完全に回避されます。 -
path
がローカルインポートである場合:build.IsLocalImport(path)
がtrue
を返します。この場合のみ、os.Getwd()
が呼び出され、現在の作業ディレクトリがsrcDir
に設定されます。これは、ローカルインポートの解決には現在の作業ディレクトリの情報が不可欠であるため、必要なコストとして許容されます。
この条件分岐により、os.Getwd()
の呼び出しは、本当に必要な場合に限定されることになります。これにより、特にGoの標準ライブラリや外部モジュールなど、ローカルインポートではないパッケージを多数インポートするようなシナリオにおいて、go/types
パッケージの処理効率が向上します。
コアとなるコードの変更箇所
変更はsrc/pkg/go/types/gcimporter.go
ファイルにあります。
--- a/src/pkg/go/types/gcimporter.go
+++ b/src/pkg/go/types/gcimporter.go
@@ -108,10 +108,14 @@ func GcImport(imports map[string]*Package, path string) (pkg *Package, err error
return Unsafe, nil
}
- srcDir, err := os.Getwd()
- if err != nil {
- return
+ srcDir := "."
+ if build.IsLocalImport(path) {
+ srcDir, err = os.Getwd()
+ if err != nil {
+ return
+ }
}
+
filename, id := FindPkg(path, srcDir)
if filename == "" {
err = errors.New("can't find import: " + id)
コアとなるコードの解説
src/pkg/go/types/gcimporter.go
ファイルは、Goコンパイラが生成するGC(Go Compiler)形式のパッケージバイナリをインポートするためのロジックを含んでいます。GcImport
関数は、指定されたパッケージパスpath
に対応するパッケージをインポートする主要な関数です。
変更前のコードでは、GcImport
関数が呼び出されるたびに、まずos.Getwd()
を呼び出して現在の作業ディレクトリを取得していました。これは、インポートパスが相対パスである場合に、その解決の基準となるディレクトリが必要だからです。しかし、path
が"unsafe"
のような組み込みパッケージや、"fmt"
のような標準ライブラリパッケージ、あるいは"github.com/some/repo"
のような絶対パス形式の外部モジュールパスである場合、現在の作業ディレクトリはパッケージの解決に直接関係ありません。
変更後のコードでは、この無条件なos.Getwd()
の呼び出しを避けるために、以下のロジックが導入されました。
srcDir := "."
:まず、srcDir
をデフォルト値の"."
(カレントディレクトリ)で初期化します。これは、FindPkg
関数が相対パスを解決する際のフォールバックとして機能します。if build.IsLocalImport(path)
:ここで、go/build
パッケージのIsLocalImport
関数を使って、インポートパスpath
がローカルインポート(例:./mypackage
)であるかどうかをチェックします。- ローカルインポートの場合:
build.IsLocalImport(path)
がtrue
を返した場合のみ、os.Getwd()
が呼び出され、実際の現在の作業ディレクトリがsrcDir
に設定されます。これは、ローカルインポートの解決には現在の作業ディレクトリが不可欠であるため、このコストは正当化されます。 - ローカルインポートではない場合:
build.IsLocalImport(path)
がfalse
を返した場合、os.Getwd()
は呼び出されません。srcDir
は初期値の"."
のままですが、FindPkg
関数はローカルインポートではないパスを解決する際に、GOPATHやGOROOTなどの環境変数で定義された標準的な検索パスを優先的に利用するため、"."
が直接的な問題となることはありません。
この変更により、os.Getwd()
の呼び出しが最小限に抑えられ、特に多数の非ローカルインポートを含むGoプログラムのコンパイルや解析のパフォーマンスが向上します。
関連リンク
- Go言語の
os
パッケージ: https://pkg.go.dev/os - Go言語の
go/types
パッケージ: https://pkg.go.dev/go/types - Go言語の
go/build
パッケージ: https://pkg.go.dev/go/build - Goのインポートパスに関するドキュメント(Go Modules以前のGOPATH時代の情報も含まれる可能性がありますが、基本的な概念は共通です): https://go.dev/doc/code
参考にした情報源リンク
- Go言語の公式ドキュメント
- Go言語のソースコード(特に
src/pkg/go/types/gcimporter.go
とsrc/pkg/go/build/build.go
) - Go言語のIssueトラッカーやメーリングリスト(
golang-dev
)での議論(コミットメッセージにgolang.org/cl/7312100
のリンクがあるため、関連するコードレビューや議論が存在する可能性が高いです) os.Getwd()
のパフォーマンスに関する一般的な情報源(オペレーティングシステムやファイルシステムの特性に関する記事など)- Go Modulesに関する情報(このコミットはGo Modules導入以前のものですが、インポートパスの解決に関する理解を深める上で役立ちます)