[インデックス 15371] ファイルの概要
このコミットは、Goコマンドラインツール(cmd/go
)におけるImportDir
関数の不要な呼び出しを排除することで、特に低速なファイルシステムやコールドキャッシュ環境下でのパフォーマンスを大幅に改善することを目的としています。具体的には、go list
コマンドの実行時間を約半分に短縮する効果が示されています。
コミット
commit ed1ac056735e67c0f6bc23c60a9c2a0f999c80cb
Author: Anthony Martin <ality@pbrane.org>
Date: Thu Feb 21 20:09:31 2013 -0800
cmd/go: don't call ImportDir unnecessarily
This significantly speeds up the go tool on
slow file systems (or those with cold caches).
The following numbers were obtained using
an encrypted ext4 file system running on
Linux 3.7.9.
# Before
$ sudo sysctl -w 'vm.drop_caches=3'
$ time go list code.google.com/p/go.net/... | wc -l
9
real 0m16.921s
user 0m0.637s
sys 0m0.317s
# After
$ sudo sysctl -w 'vm.drop_caches=3'
$ time go list code.google.com/p/go.net/... | wc -l
9
real 0m8.175s
user 0m0.220s
sys 0m0.177s
R=rsc, r
CC=golang-dev
https://golang.org/cl/7369044
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/ed1ac056735e67c0f6bc23c60a9c2a0f999c80cb
元コミット内容
cmd/go: don't call ImportDir unnecessarily
This significantly speeds up the go tool on
slow file systems (or those with cold caches).
The following numbers were obtained using
an encrypted ext4 file system running on
Linux 3.7.9.
# Before
$ sudo sysctl -w 'vm.drop_caches=3'
$ time go list code.google.com/p/go.net/... | wc -l
9
real 0m16.921s
user 0m0.637s
sys 0m0.317s
# After
$ sudo sysctl -w 'vm.drop_caches=3'
$ time go list code.google.com/p/go.net/... | wc -l
9
real 0m8.175s
user 0m0.220s
sys 0m0.177s
R=rsc, r
CC=golang-dev
https://golang.org/cl/7369044
変更の背景
Goツール、特にgo list
コマンドは、Goパッケージの情報を取得するためにファイルシステムを頻繁に操作します。この操作には、パッケージのインポートパスを解決し、そのディレクトリにGoのソースファイルが存在するかどうかを確認するために、go/build
パッケージのImportDir
関数が使用されます。
しかし、元の実装では、特定の条件(例えば、既に処理済みと判断されたパッケージや、パターンに一致しないパッケージ)であっても、無条件にImportDir
が呼び出されていました。ImportDir
は、指定されたディレクトリをスキャンし、Goパッケージとして有効かどうかを判断するためにファイルシステムへのアクセスを伴います。
低速なファイルシステム(例:暗号化されたファイルシステム、ネットワークファイルシステム)や、OSのファイルキャッシュがクリアされた「コールドキャッシュ」状態では、このような不要なファイルシステムアクセスがパフォーマンスのボトルネックとなっていました。コミットメッセージに示されているように、sysctl -w 'vm.drop_caches=3'
コマンドでキャッシュをクリアした後のベンチマークでは、go list
コマンドの実行に16秒以上かかっていました。
このパフォーマンス問題を解決するため、ImportDir
の呼び出しを、実際にパッケージ情報が必要な場合にのみ行うように最適化する必要がありました。
前提知識の解説
このコミットを理解するためには、以下の概念について理解しておく必要があります。
go list
コマンド: Goの標準ツールの一つで、Goパッケージに関する情報を表示します。例えば、パッケージの依存関係、ビルド情報、ソースファイルのパスなどを取得できます。go list <import path>
のように使用し、<import path>
にはパッケージのインポートパスを指定します。...
を付けることで、指定されたパス以下の全てのパッケージを再帰的にリストアップできます。go/build
パッケージ: Goの標準ライブラリの一部で、Goのパッケージビルドプロセスに関する情報を提供します。Goのソースコードを解析し、パッケージの依存関係を解決し、ビルド可能なパッケージを特定する機能を提供します。build.Context.ImportDir(path string, mode build.ImportMode)
:go/build
パッケージ内の関数で、指定されたディレクトリパス(path
)をGoパッケージとしてインポートしようと試みます。この関数は、ディレクトリ内のGoソースファイルをスキャンし、パッケージのメタデータを構築します。もし指定されたディレクトリが有効なGoパッケージでない場合や、エラーが発生した場合はエラーを返します。この操作はファイルシステムへのアクセスを伴うため、パフォーマンスに影響を与える可能性があります。- ファイルシステムキャッシュと
sysctl -w 'vm.drop_caches=3'
: 現代のオペレーティングシステムは、ディスクI/Oのパフォーマンスを向上させるために、ファイルシステムキャッシュ(ページキャッシュ、ディレクトリキャッシュ、inodeキャッシュなど)を使用します。これにより、一度読み込んだデータはメモリに保持され、再度のアクセスが高速になります。sudo sysctl -w 'vm.drop_caches=3'
コマンドは、Linuxシステムにおいて、これらのファイルシステムキャッシュを強制的にクリアするコマンドです。1
: ページキャッシュをクリア2
: inodeとdentry(ディレクトリキャッシュ)をクリア3
: ページキャッシュ、inode、dentryの全てをクリア このコマンドは、ディスクI/Oの純粋なパフォーマンスを測定するベンチマークなどで、キャッシュの影響を排除するために使用されます。コミットメッセージのベンチマークでは、コールドキャッシュ状態でのパフォーマンスを測定するためにこのコマンドが使用されています。
filepath.SkipDir
:filepath.WalkFunc
(filepath.Walk
関数に渡されるコールバック関数)が返すエラー値の一つです。このエラーを返すと、filepath.Walk
は現在のディレクトリのサブディレクトリへの再帰的な探索をスキップします。
技術的詳細
このコミットの核心は、src/cmd/go/main.go
内のmatchPackages
関数におけるbuildContext.ImportDir
の呼び出しロジックの変更です。
matchPackages
関数は、与えられたパターンに一致するGoパッケージを検索し、そのインポートパスのリストを返します。この関数は、ファイルシステムをウォーク(走査)し、各ディレクトリがGoパッケージであるかどうかを判断します。
変更前のコードでは、ディレクトリを走査する際に、まずname = "cmd/" + name
のように擬似的なインポートパスを構築し、have
マップで既に処理済みかどうかを確認していました。しかし、ImportDir
の呼び出しは、have[name]
がtrue
(つまり既に処理済み)であるかどうかのチェックの後に行われていました。さらに、match(name)
(パターンに一致するかどうか)のチェックもImportDir
の呼び出しの後に行われていました。
これは、以下の問題を引き起こしていました。
- 不要な
ImportDir
呼び出し:- 既に
have
マップに存在する(つまり、以前に処理された)パッケージであっても、ImportDir
が呼び出されていました。 match(name)
がfalse
(つまり、パターンに一致しない)パッケージであっても、ImportDir
が呼び出されていました。ImportDir
はファイルシステムをスキャンするため、これらの不要な呼び出しはI/Oオーバーヘッドを発生させ、特に低速なファイルシステムやコールドキャッシュ環境で顕著なパフォーマンス低下を招いていました。
- 既に
変更後のコードでは、ImportDir
の呼び出しを、実際にそのパッケージが必要とされる条件(まだ処理されておらず、かつパターンに一致する場合)を満たした後に移動させました。
具体的には、以下の順序でチェックが行われるようになりました。
- 擬似的なインポートパス
name
を構築。 have[name]
で既に処理済みかどうかをチェック。もし処理済みであれば、そのディレクトリの処理をスキップ(return nil
)。have[name] = true
を設定し、このパッケージが処理中であることをマーク。!match(name)
でパターンに一致しないかどうかをチェック。もし一致しなければ、そのディレクトリの処理をスキップ(return nil
)。- 上記のチェックを通過した場合にのみ、
buildContext.ImportDir(path, 0)
を呼び出す。
この変更により、ImportDir
は本当に必要な場合にのみ実行されるようになり、ファイルシステムへの不要なアクセスが劇的に減少しました。結果として、go list
コマンドの実行時間が大幅に短縮されました。コミットメッセージのベンチマーク結果がその効果を明確に示しています。
コアとなるコードの変更箇所
src/cmd/go/main.go
ファイルの matchPackages
関数内の変更です。
--- a/src/cmd/go/main.go
+++ b/src/cmd/go/main.go
@@ -453,19 +453,20 @@ func matchPackages(pattern string) []string {
return filepath.SkipDir
}
+ // We use, e.g., cmd/gofmt as the pseudo import path for gofmt.
+ name = "cmd/" + name
+ if have[name] {
+ return nil
+ }
+ have[name] = true
+ if !match(name) {
+ return nil
+ }
_, err = buildContext.ImportDir(path, 0)
if err != nil {
return nil
}
-
- // We use, e.g., cmd/gofmt as the pseudo import path for gofmt.
- name = "cmd/" + name
- if !have[name] {
- have[name] = true
- if match(name) {
- pkgs = append(pkgs, name)
- }
- }
+ pkgs = append(pkgs, name)
return nil
})
@@ -493,14 +494,14 @@ func matchPackages(pattern string) []string {
return nil
}
have[name] = true
-
+ if !match(name) {
+ return nil
+ }
_, err = buildContext.ImportDir(path, 0)
if err != nil && strings.Contains(err.Error(), "no Go source files") {
return nil
}
- if match(name) {
- pkgs = append(pkgs, name)
- }
+ pkgs = append(pkgs, name)
return nil
})
}
コアとなるコードの解説
変更は主にmatchPackages
関数内のfilepath.Walk
コールバック関数と、その後の匿名関数内で行われています。
1. filepath.Walk
コールバック内の変更 (行 453-472)
-
変更前:
_, err = buildContext.ImportDir(path, 0) if err != nil { return nil } // We use, e.g., cmd/gofmt as the pseudo import path for gofmt. name = "cmd/" + name if !have[name] { have[name] = true if match(name) { pkgs = append(pkgs, name) } }
ここでは、まず
ImportDir
を呼び出し、その後にname
を構築し、have
マップとmatch
関数でフィルタリングしていました。つまり、ImportDir
は常に呼び出されていました。 -
変更後:
// We use, e.g., cmd/gofmt as the pseudo import path for gofmt. name = "cmd/" + name if have[name] { // 既に処理済みならスキップ return nil } have[name] = true // 処理中としてマーク if !match(name) { // パターンに一致しないならスキップ return nil } _, err = buildContext.ImportDir(path, 0) // ここで初めてImportDirを呼び出す if err != nil { return nil } pkgs = append(pkgs, name) // パッケージリストに追加
この変更により、
ImportDir
の呼び出しが、have
マップでの重複チェックとmatch
関数でのパターン一致チェックの後に移動しました。これにより、既に処理されたパッケージや、検索パターンに一致しないパッケージに対してはImportDir
が呼び出されなくなり、ファイルシステムへの不要なアクセスが削減されます。
2. 匿名関数内の変更 (行 493-507)
-
変更前:
have[name] = true _, err = buildContext.ImportDir(path, 0) if err != nil && strings.Contains(err.Error(), "no Go source files") { return nil } if match(name) { pkgs = append(pkgs, name) }
ここでも、
ImportDir
の呼び出しがmatch(name)
のチェックより前に行われていました。 -
変更後:
have[name] = true if !match(name) { // パターンに一致しないならスキップ return nil } _, err = buildContext.ImportDir(path, 0) // ここで初めてImportDirを呼び出す if err != nil && strings.Contains(err.Error(), "no Go source files") { return nil } pkgs = append(pkgs, name) // パッケージリストに追加
同様に、
match(name)
のチェックをImportDir
の呼び出しの前に移動させることで、パターンに一致しないパッケージに対する不要なImportDir
呼び出しを回避しています。
これらの変更は、Goツールがパッケージ情報を収集する際の効率を大幅に向上させ、特にI/O性能がボトルネックとなる環境でのユーザーエクスペリエンスを改善しました。
関連リンク
参考にした情報源リンク
- Go言語公式ドキュメント:
go/build
パッケージ - Go言語公式ドキュメント:
cmd/go
コマンド - Linux
sysctl
manページ - Linux
drop_caches
ドキュメント