[インデックス 17141] ファイルの概要
このコミットは、Go言語のcmd/api
ツールにおけるパッケージインポート処理の重複作業を排除し、パフォーマンスを大幅に改善することを目的としています。具体的には、パッケージのインポート結果をキャッシュすることで、APIチェックにかかる時間を短縮し、同時にビルドタグに関する混乱を解消しています。
コミット
commit b78410bda13cc10c1e59dfdcc935b3155450b44e
Author: Russ Cox <rsc@golang.org>
Date: Fri Aug 9 18:44:00 2013 -0400
cmd/api: eliminate duplicate package import work
On my Mac, cuts the API checks from 15 seconds to 6 seconds.
Also clean up some tag confusion: go run list-of-files ignores tags.
R=bradfitz, gri
CC=golang-dev
https://golang.org/cl/12699048
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/b78410bda13cc10c1e59dfdcc935b3155450b44e
元コミット内容
cmd/api
ツールにおいて、重複するパッケージインポート作業を排除する。
これにより、Mac上でのAPIチェック時間が15秒から6秒に短縮された。
また、タグに関する混乱(go run
がファイルリストを無視するタグ)も解消された。
変更の背景
Go言語のcmd/api
ツールは、GoパッケージのエクスポートされたAPIを計算するために使用されます。このツールは、Goの標準ライブラリやその他のパッケージのAPIが、Goのバージョンアップによって意図せず変更されていないかを確認するために、GoプロジェクトのCI/CDパイプラインや開発プロセスにおいて重要な役割を果たします。
このコミットが導入される以前は、cmd/api
ツールがパッケージをインポートする際に、同じパッケージであっても異なるビルドコンテキスト(例えば、異なるOSやアーキテクチャ、あるいは特定のビルドタグが有効になっているかどうか)で複数回インポートされる場合、その都度、パッケージの解析や型チェックといった時間のかかる処理が重複して実行されていました。これは特に、多数のパッケージを扱う場合や、異なるビルドコンテキストでAPIをチェックする必要がある場合に、ツールの実行時間を著しく長くしていました。コミットメッセージにある「Mac上でAPIチェックが15秒から6秒に短縮された」という記述は、この重複作業がパフォーマンスに与えていた影響の大きさを物語っています。
また、このコミットは「タグに関する混乱」も解消しています。具体的には、go run
コマンドが特定のビルドタグ(例: from_src_run
)をどのように扱うか、あるいは無視するかについての挙動が不明瞭であったり、意図しない結果を引き起こしたりする可能性がありました。go run
は通常、指定されたGoソースファイルをコンパイルして実行しますが、ビルドタグはコンパイル時にどのファイルを含めるかを制御します。このコミットでは、cmd/api/run.go
のビルドタグを+build ignore
に変更し、src/run.bash
およびsrc/run.bat
から--tags=from_src_run
オプションを削除することで、cmd/api
ツールの実行方法を簡素化し、ビルドタグの適用に関する混乱を解消しています。これにより、cmd/api
ツールがより予測可能で効率的に動作するようになりました。
前提知識の解説
このコミットの理解には、以下のGo言語の概念とツールに関する知識が不可欠です。
-
go run
コマンド:go run
は、Goのソースファイルをコンパイルし、その結果生成された実行可能ファイルを一時的に実行するためのコマンドです。開発中に単一のGoプログラムを素早くテストする際によく使用されます。通常、コンパイルされたバイナリは一時ディレクトリに保存され、実行後に削除されます。 -
cmd/api
ツール: Goのソースコードリポジトリに含まれる内部ツールの一つで、GoパッケージのエクスポートされたAPI(公開されている型、関数、変数など)を抽出・比較するために使用されます。Go言語の互換性保証(Go 1互換性ポリシーなど)を維持するために、Goのリリースプロセスにおいて重要な役割を果たします。このツールは、go/build
パッケージとgo/types
パッケージを利用して、Goのソースコードを解析し、API情報を抽出します。 -
go/build
パッケージ: Goの標準ライブラリの一部で、Goパッケージのビルドプロセスに関する情報を提供します。これには、ソースファイルの検索、パッケージの依存関係の解決、ビルドタグの処理などが含まれます。build.Context
構造体は、特定のビルド環境(OS、アーキテクチャ、ビルドタグなど)を表現し、パッケージのインポート時に使用されます。 -
go/types
パッケージ: Goの標準ライブラリの一部で、Goプログラムの型チェックを行うための機能を提供します。このパッケージは、Goのソースコードを抽象構文木(AST)として解析し、そのASTに対して型情報を付与し、型エラーを検出します。cmd/api
ツールは、このパッケージを使用して、パッケージ内のエクスポートされたシンボルの型情報を正確に把握します。 -
Goのビルドタグ (Build Tags): Goのソースファイルには、
// +build tag
のようなコメント行を追加することで、ビルドタグを指定できます。これにより、特定のタグが有効な場合にのみ、そのソースファイルがコンパイルに含まれるように制御できます。例えば、// +build linux
はLinuxシステムでのみコンパイルされるファイルを示します。ビルドタグは、プラットフォーム固有のコードや、特定の機能の有効/無効を切り替えるために使用されます。 -
Goのパッケージインポートプロセス: Goプログラムが
import
ステートメントを使用して他のパッケージを参照すると、Goツールチェーンは以下の手順でパッケージを解決し、ロードします。- パッケージの検索:
GOPATH
やGOROOT
などの環境変数に基づいて、指定されたパッケージ名のソースコードディレクトリを検索します。 - ビルドコンテキストの適用: 現在のOS、アーキテクチャ、および有効なビルドタグに基づいて、どのソースファイルがコンパイルに含まれるべきかを決定します。
- ソースファイルの解析: 選択されたソースファイルを解析し、抽象構文木(AST)を構築します。
- 型チェック:
go/types
パッケージなどを使用して、ASTに対して型チェックを実行し、パッケージのエクスポートされたAPIを含む型情報を生成します。 このプロセスは、特に大規模なプロジェクトや、多くの依存関係を持つパッケージの場合、時間とリソースを消費します。
- パッケージの検索:
技術的詳細
このコミットの主要な技術的変更点は、cmd/api
ツールにおけるパッケージインポートのキャッシュメカニズムの導入と、ビルドタグの扱いに関する改善です。
1. パッケージインポートのキャッシュ
src/cmd/api/goapi.go
に、以下の2つのグローバルマップが導入されました。
-
pkgCache = map[string]*types.Package{}
: これは、既にインポートされ、型チェックが完了したtypes.Package
オブジェクトをキャッシュするためのマップです。キーは、パッケージのディレクトリと、そのパッケージのインポートに関連するビルドタグの組み合わせから生成される一意の文字列(tagKey
関数によって生成)です。これにより、同じビルドコンテキストで同じパッケージが再度インポートされる際に、高コストな解析と型チェックのプロセスをスキップし、キャッシュされた結果を再利用できます。 -
pkgTags = map[string][]string{}
: これは、各パッケージディレクトリに関連するすべてのビルドタグのリストを保存するためのマップです。キーはパッケージのディレクトリパスです。go/build
パッケージのPackage.AllTags
フィールドから取得されるこのタグリストは、tagKey
を生成する際に、どのタグが現在のビルドコンテキストで「関連する」かを判断するために使用されます。
tagKey
関数の導入
tagKey
関数は、キャッシュのキーを生成する中心的な役割を担います。この関数は、パッケージのディレクトリパス、現在のビルドコンテキスト(build.Context
)、およびそのパッケージに関連するすべてのビルドタグのリストを受け取ります。
関数内部では、現在のビルドコンテキストで有効なタグ(GOOS
, GOARCH
, cgo
, BuildTags
)をセットとして構築し、pkgTags
から取得したパッケージ固有のタグリストの中から、この有効なタグセットに含まれるタグのみを抽出します。これらのタグはソートされた順序でディレクトリパスにカンマ区切りで連結され、一意のキャッシュキーが生成されます。これにより、同じパッケージであっても、異なるビルドコンテキスト(例えば、linux,amd64
とwindows,amd64
)では異なるキャッシュキーが生成され、適切なパッケージバージョンがキャッシュから取得されるようになります。
Walker.Import
メソッドの変更
Walker.Import
メソッドは、cmd/api
ツールがGoパッケージをインポートする際の主要なエントリポイントです。このメソッドに以下のキャッシュロジックが追加されました。
-
キャッシュのチェック: パッケージのインポートが要求された際、まず
pkgTags
マップをチェックして、そのディレクトリが以前にインポートされたことがあるかを確認します。もしあれば、tagKey
関数を使用してキャッシュキーを生成し、pkgCache
マップにそのキーが存在するかどうかを調べます。存在し、かつnil
でない(つまり、インポート処理中ではない)場合は、キャッシュされたtypes.Package
オブジェクトが即座に返され、重複する作業が回避されます。 -
build.Context.ImportDir
の呼び出し: キャッシュミスが発生した場合、または初回インポートの場合にのみ、context.ImportDir
が呼び出され、実際のパッケージ情報がファイルシステムから読み込まれます。 -
pkgTags
の更新: パッケージディレクトリが初めてインポートされる場合、info.AllTags
(build.Package
から取得される、そのパッケージに関連するすべてのビルドタグのリスト)がpkgTags
マップに保存されます。これにより、将来的に同じディレクトリがインポートされる際に、tagKey
を正確に生成できるようになります。 -
pkgCache
への保存: パッケージの型チェックが正常に完了した後、生成されたtypes.Package
オブジェクトは、tagKey
によって生成されたキーを使用してpkgCache
に保存されます。これにより、後続のインポート要求で再利用できるようになります。
2. ビルドタグのクリーンアップ
このコミットでは、src/cmd/api/run.go
、src/run.bash
、src/run.bat
におけるビルドタグの扱いも変更されました。
-
src/cmd/api/run.go
の変更: ファイルの先頭にあった// +build from_src_run
というビルドタグが// +build ignore
に変更されました。+build ignore
は、そのファイルがGoのビルドプロセスから完全に無視されることを意味します。これは、src/cmd/api/run.go
がgo run
コマンドによって直接実行されることを意図しており、通常のgo build
プロセスでコンパイルされるべきではないことを明確に示しています。 -
src/run.bash
とsrc/run.bat
の変更: これらのスクリプトは、Goのテストスイートの一部としてcmd/api
ツールを実行するために使用されます。以前は、go run --tags=from_src_run $GOROOT/src/cmd/api/run.go
のように、--tags=from_src_run
オプションを明示的に指定してgo run
を呼び出していました。このコミットでは、このオプションが削除され、単にgo run $GOROOT/src/cmd/api/run.go
と呼び出すようになりました。 この変更は、src/cmd/api/run.go
が+build ignore
になったことと合わせて、cmd/api
ツールの実行方法を簡素化し、ビルドタグの適用に関する混乱を解消することを目的としています。go run
は、指定されたファイルを直接実行するため、そのファイルに+build ignore
があっても実行できます。これにより、cmd/api
ツールが特定のビルドタグに依存することなく、より直接的に実行されるようになりました。
これらの変更により、cmd/api
ツールのパフォーマンスが向上し、ビルドシステムにおけるタグの扱いがより明確になりました。
コアとなるコードの変更箇所
src/cmd/api/goapi.go
--- a/src/cmd/api/goapi.go
+++ b/src/cmd/api/goapi.go
@@ -1,9 +1,9 @@
-// +build api_tool
-
// Copyright 2011 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
+// +build api_tool
+
// Binary api computes the exported API of a set of Go packages.
package main
@@ -387,6 +387,38 @@ func contains(list []string, s string) bool {
return false
}
+var (
+ pkgCache = map[string]*types.Package{} // map tagKey to package
+ pkgTags = map[string][]string{} // map import dir to list of relevant tags
+)
+
+// tagKey returns the tag-based key to use in the pkgCache.
+// It is a comma-separated string; the first part is dir, the rest tags.
+// The satisfied tags are derived from context but only those that
+// matter (the ones listed in the tags argument) are used.
+// The tags list, which came from go/build's Package.AllTags,
+// is known to be sorted.
+func tagKey(dir string, context *build.Context, tags []string) string {
+ ctags := map[string]bool{
+ context.GOOS: true,
+ context.GOARCH: true,
+ }
+ if context.CgoEnabled {
+ ctags["cgo"] = true
+ }
+ for _, tag := range context.BuildTags {
+ ctags[tag] = true
+ }
+ // TODO: ReleaseTags (need to load default)
+ key := dir
+ for _, tag := range tags {
+ if ctags[tag] {
+ key += "," + tag
+ }
+ }
+ return key
+}
+
// Importing is a sentinel taking the place in Walker.imported
// for a package that is in the process of being imported.\
var importing types.Package
@@ -411,6 +443,19 @@ func (w *Walker) Import(name string) (pkg *types.Package) {
if context == nil {
context = &build.Default
}
+
+ // Look in cache.
+ // If we've already done an import with the same set
+ // of relevant tags, reuse the result.
+ var key string
+ if tags, ok := pkgTags[dir]; ok {
+ key = tagKey(dir, context, tags)
+ if pkg := pkgCache[key]; pkg != nil {
+ w.imported[name] = pkg
+ return pkg
+ }
+ }
+
info, err := context.ImportDir(dir, 0)
if err != nil {
if _, nogo := err.(*build.NoGoError); nogo {
@@ -418,6 +463,13 @@ func (w *Walker) Import(name string) (pkg *types.Package) {
}
log.Fatalf("pkg %q, dir %q: ScanDir: %v", name, dir, err)
}\
+
+ // Save tags list first time we see a directory.
+ if _, ok := pkgTags[dir]; !ok {
+ pkgTags[dir] = info.AllTags
+ key = tagKey(dir, context, info.AllTags)
+ }
+
filenames := append(append([]string{}, info.GoFiles...), info.CgoFiles...)\
// Certain files only exist when building for the specified context.\
@@ -463,6 +515,8 @@ func (w *Walker) Import(name string) (pkg *types.Package) {
log.Fatalf("error typechecking package %s: %s (%s)", name, err, ctxt)
}
+ pkgCache[key] = pkg
+
w.imported[name] = pkg
return
}
src/cmd/api/run.go
--- a/src/cmd/api/run.go
+++ b/src/cmd/api/run.go
@@ -1,9 +1,9 @@
-// +build from_src_run
-
// Copyright 2013 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
+// +build ignore
+
// The run program is invoked via "go run" from src/run.bash or
// src/run.bat conditionally builds and runs the cmd/api tool.
//
src/run.bash
--- a/src/run.bash
+++ b/src/run.bash
@@ -182,7 +182,7 @@ time go run run.go || exit 1
echo
echo '# Checking API compatibility.'
-go run --tags=from_src_run $GOROOT/src/cmd/api/run.go
+time go run $GOROOT/src/cmd/api/run.go
echo
echo ALL TESTS PASSED
src/run.bat
--- a/src/run.bat
+++ b/src/run.bat
@@ -121,7 +121,7 @@ set GOMAXPROCS=%OLDGOMAXPROCS%\n set OLDGOMAXPROCS=\n \n echo # Checking API compatibility.\n-go run --tags=from_src_run "%GOROOT%\\src\\cmd\\api\\run.go"\n+go run "%GOROOT%\\src\\cmd\\api\\run.go"\n if errorlevel 1 goto fail\n echo.\n \n```
## コアとなるコードの解説
### `src/cmd/api/goapi.go`の変更
1. **グローバル変数の追加**:
`pkgCache`と`pkgTags`という2つのグローバルマップが追加されました。
* `pkgCache`: `tagKey`(文字列)をキーとし、`*types.Package`(型チェック済みのパッケージ情報)を値とするマップです。これにより、同じビルドコンテキストでインポートされたパッケージの情報を再利用できます。
* `pkgTags`: パッケージのディレクトリパス(文字列)をキーとし、そのパッケージに関連するすべてのビルドタグのリスト(`[]string`)を値とするマップです。これは、`tagKey`を生成する際に、そのパッケージにどのタグが関連しているかを効率的に取得するために使用されます。
2. **`tagKey`関数の追加**:
この関数は、パッケージのディレクトリと現在のビルドコンテキスト(OS、アーキテクチャ、Cgoの有効/無効、カスタムビルドタグ)に基づいて、キャッシュキーを生成します。
* `ctags`マップは、現在のビルドコンテキストで有効なタグを効率的にルックアップするために使用されます。
* 引数`tags`は、`go/build`パッケージから取得した、そのパッケージに関連する可能性のあるすべてのタグのリストです。
* `key`文字列は、まずディレクトリパスで初期化され、その後、`tags`リストの中から`ctags`マップに存在する(つまり、現在のビルドコンテキストで有効な)タグがカンマ区切りで追加されます。これにより、同じパッケージでもビルドコンテキストが異なれば異なるキーが生成され、適切なキャッシュエントリが選択されるようになります。
3. **`Walker.Import`メソッドの変更**:
このメソッドは、パッケージをインポートする際の主要なロジックを含んでいます。
* **キャッシュの利用**: メソッドの冒頭で、`pkgTags`と`pkgCache`を利用して、既に同じビルドコンテキストでこのパッケージがインポートされていないかを確認します。
* `if tags, ok := pkgTags[dir]; ok`: まず、このディレクトリが以前にインポートされたことがあり、関連タグが`pkgTags`に保存されているかを確認します。
* `key = tagKey(dir, context, tags)`: 関連タグが見つかった場合、`tagKey`関数を使ってキャッシュキーを生成します。
* `if pkg := pkgCache[key]; pkg != nil`: 生成されたキーで`pkgCache`をチェックし、キャッシュされたパッケージが存在すれば、それを`w.imported`に設定してすぐに返します。これにより、重複するインポート作業が回避されます。
* **`pkgTags`の更新**: `context.ImportDir`が呼び出され、パッケージ情報が実際に読み込まれた後、そのディレクトリが`pkgTags`にまだ存在しない場合(つまり、そのディレクトリが初めてインポートされる場合)、`info.AllTags`(`build.Package`から取得される、そのパッケージに関連するすべてのタグ)が`pkgTags`に保存されます。これにより、将来のインポートで`tagKey`を正確に生成できるようになります。
* **`pkgCache`への保存**: パッケージの型チェックが完了し、`pkg`オブジェクトが生成された後、`pkgCache[key] = pkg`という行が追加され、生成されたパッケージがキャッシュに保存されます。
### `src/cmd/api/run.go`の変更
* ファイルの先頭のビルドタグが`// +build from_src_run`から`// +build ignore`に変更されました。`+build ignore`は、`go build`コマンドがこのファイルを無視することを意味します。これは、このファイルが`go run`によって直接実行されることを意図しているためです。
### `src/run.bash`と`src/run.bat`の変更
* `go run`コマンドの呼び出しから`--tags=from_src_run`オプションが削除されました。これは、`src/cmd/api/run.go`が`+build ignore`になったため、特定のビルドタグを指定する必要がなくなったためです。これにより、スクリプトが簡素化され、ビルドタグの扱いに関する混乱が解消されました。
これらの変更により、`cmd/api`ツールは、同じパッケージを異なるビルドコンテキストで複数回処理する際に、高コストなインポートと型チェックの作業を大幅に削減できるようになり、実行パフォーマンスが劇的に向上しました。また、ビルドタグの扱いもより明確で予測可能なものになりました。
## 関連リンク
* [https://golang.org/cl/12699048](https://golang.org/cl/12699048)
## 参考にした情報源リンク
* Go言語の公式ドキュメント(`go/build`パッケージ、`go/types`パッケージ、`go run`コマンドに関する情報)
* Go言語のビルドタグに関する情報
* Go言語のパッケージインポートメカニズムに関する一般的な知識
* `cmd/api`ツールの目的と機能に関する情報
* Goのソースコード(特に`src/cmd/api`ディレクトリ)
* Goのコミット履歴と関連する議論