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

[インデックス 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のようなパスはローカルインポートと判定されますが、fmtgithub.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
    }
}

この変更により、以下のロジックが適用されます。

  1. pathがローカルインポートではない場合: build.IsLocalImport(path)falseを返します。この場合、os.Getwd()は呼び出されず、srcDirは初期値の"."のままとなります。FindPkg関数は、この"."を基点としてパッケージを検索しますが、ローカルインポートではないため、通常はGOPATHやGOROOTなどの標準的な検索パスで解決されます。os.Getwd()の呼び出しコストが完全に回避されます。

  2. 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()の呼び出しを避けるために、以下のロジックが導入されました。

  1. srcDir := ".":まず、srcDirをデフォルト値の"."(カレントディレクトリ)で初期化します。これは、FindPkg関数が相対パスを解決する際のフォールバックとして機能します。
  2. if build.IsLocalImport(path):ここで、go/buildパッケージのIsLocalImport関数を使って、インポートパスpathがローカルインポート(例: ./mypackage)であるかどうかをチェックします。
  3. ローカルインポートの場合: build.IsLocalImport(path)trueを返した場合のみ、os.Getwd()が呼び出され、実際の現在の作業ディレクトリがsrcDirに設定されます。これは、ローカルインポートの解決には現在の作業ディレクトリが不可欠であるため、このコストは正当化されます。
  4. ローカルインポートではない場合: build.IsLocalImport(path)falseを返した場合、os.Getwd()は呼び出されません。srcDirは初期値の"."のままですが、FindPkg関数はローカルインポートではないパスを解決する際に、GOPATHやGOROOTなどの環境変数で定義された標準的な検索パスを優先的に利用するため、"."が直接的な問題となることはありません。

この変更により、os.Getwd()の呼び出しが最小限に抑えられ、特に多数の非ローカルインポートを含むGoプログラムのコンパイルや解析のパフォーマンスが向上します。

関連リンク

参考にした情報源リンク

  • Go言語の公式ドキュメント
  • Go言語のソースコード(特にsrc/pkg/go/types/gcimporter.gosrc/pkg/go/build/build.go
  • Go言語のIssueトラッカーやメーリングリスト(golang-dev)での議論(コミットメッセージにgolang.org/cl/7312100のリンクがあるため、関連するコードレビューや議論が存在する可能性が高いです)
  • os.Getwd()のパフォーマンスに関する一般的な情報源(オペレーティングシステムやファイルシステムの特性に関する記事など)
  • Go Modulesに関する情報(このコミットはGo Modules導入以前のものですが、インポートパスの解決に関する理解を深める上で役立ちます)