[インデックス 17552] ファイルの概要
このコミットは、Goコマンドラインツール(cmd/go
)におけるパッケージ探索の効率化を目的としています。具体的には、パターンマッチングに基づいてファイルツリーのウォーク(走査)を枝刈り(prune)する機能を追加し、不要なディレクトリの探索をスキップすることで、go list
などのコマンドのパフォーマンスを向上させています。
コミット
commit e6a49555a723e176dbcc45ca9201006575fd3e56
Author: Russ Cox <rsc@golang.org>
Date: Wed Sep 11 09:57:05 2013 -0400
cmd/go: use pattern to prune file tree walk
For example, if the pattern is m... there is
no need to look in directories not beginning with m.
Fixes #5214.
R=golang-dev, adg
CC=golang-dev
https://golang.org/cl/13253049
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/e6a49555a723e176dbcc45ca9201006575fd3e56
元コミット内容
cmd/go: use pattern to prune file tree walk
このコミットは、cmd/go
ツールがファイルツリーを走査する際に、指定されたパターンに基づいて不要な探索を枝刈りするように変更します。例えば、パターンがm...
(m
で始まる任意のパッケージ)である場合、m
で始まらないディレクトリを探索する必要がなくなります。
この変更は、Issue #5214 を修正します。
変更の背景
Goのパッケージ管理システムでは、go list
やgo build
などのコマンドが、GOPATH
やGOROOT
配下のディレクトリを走査してパッケージを探します。この走査は、特に大規模なプロジェクトや多数のパッケージが存在する環境では、時間がかかるボトルネックとなる可能性がありました。
元の実装では、パッケージのパターンマッチングを行う際に、すべてのディレクトリを走査してから、その結果に対してパターンを適用していました。これは、パターンに合致しないことが明らかなディレクトリであっても、無駄に走査してしまうことを意味します。
このコミットの背景にあるのは、このような非効率性を解消し、パッケージ探索のパフォーマンスを向上させることです。具体的には、パターンが与えられた場合に、そのパターンに合致する可能性がないディレクトリを早期にスキップすることで、ファイルシステムへのアクセス回数を減らし、コマンドの実行時間を短縮することを目指しています。
Issue #5214 は、まさにこのパフォーマンスの問題を指摘しており、go list ...
のようなコマンドが非常に遅いという報告がなされていました。このコミットは、その問題に対する直接的な解決策を提供します。
前提知識の解説
Goコマンド (cmd/go
)
cmd/go
は、Go言語のビルド、テスト、パッケージ管理などを行うための公式コマンドラインツールです。開発者はこのツールを使って、Goプログラムのコンパイル、依存関係の管理、テストの実行などを行います。
go list
コマンド
go list
コマンドは、Goパッケージに関する情報を表示するために使用されます。例えば、go list all
はすべての利用可能なパッケージをリストアップし、go list <pattern>
は指定されたパターンに合致するパッケージをリストアップします。このコマンドは、パッケージの依存関係の解析や、ビルドシステムの自動化など、様々な場面で利用されます。
パッケージパターン
Goコマンドでは、パッケージを指定する際にパターンを使用できます。
.
: 現在のディレクトリのパッケージ...
: 現在のディレクトリとそのサブディレクトリ内のすべてのパッケージnet
:net
パッケージnet/http
:net/http
パッケージnet...
:net
で始まるすべてのパッケージ(例:net
,net/http
,netchan
など)net/...
:net
ディレクトリとそのサブディレクトリ内のすべてのパッケージ(例:net/http
,net/rpc
など)
ファイルツリーウォーク (File Tree Walk)
ファイルツリーウォークとは、ファイルシステム上のディレクトリ構造を再帰的に探索する処理のことです。Go言語では、filepath.Walk
関数などがこの目的で使用されます。この処理は、特定のファイルやディレクトリを探したり、ファイルシステム全体の情報を収集したりする際に必要となります。
枝刈り (Pruning)
枝刈りとは、探索アルゴリズムにおいて、不要な探索パスを早期に打ち切る最適化手法です。ファイルツリーウォークにおいては、特定の条件に基づいて、そのディレクトリ以下を探索しても目的の結果が得られないことが分かった場合に、そのディレクトリの探索をスキップすることを指します。これにより、探索範囲が大幅に削減され、パフォーマンスが向上します。
技術的詳細
このコミットの主要な変更点は、cmd/go/main.go
にhasPathPrefix
とtreeCanMatchPattern
という2つの新しいヘルパー関数を導入し、これらをmatchPackages
関数内で利用することで、ファイルツリーウォークの枝刈りを実現している点です。
-
hasPathPrefix(s, prefix string) bool
: この関数は、文字列s
がprefix
で始まるかどうかを報告します。ただし、単なるstrings.HasPrefix
とは異なり、パスのセパレータ(/
)を考慮して、パスの要素としてprefix
がs
のプレフィックスになっているかを厳密にチェックします。 例えば、hasPathPrefix("net/http", "net")
はtrue
を返しますが、hasPathPrefix("netchan", "net")
はfalse
を返します。これは、netchan
がnet
で始まるものの、net
が独立したパス要素ではないためです。 -
treeCanMatchPattern(pattern string) func(name string) bool
: この関数は、与えられたpattern
に基づいて、特定のname
(ディレクトリ名またはパッケージ名)またはその子孫がパターンに合致する可能性があるかどうかを判断するクロージャを返します。- パターンに
...
(ワイルドカード)が含まれる場合、wildCard
フラグがtrue
になります。 - 返されるクロージャは、以下のいずれかの条件が満たされる場合に
true
を返します。name
の長さがpattern
の長さ以下であり、かつpattern
がname
のパスプレフィックスである場合。これは、pattern
がname
の親ディレクトリであるか、name
自身である場合に該当します。例えば、pattern="net/http"
、name="net"
の場合など。wildCard
がtrue
であり、かつname
がpattern
のプレフィックスである場合。これは、pattern
がnet...
のような形式で、name
がnet
で始まる場合に該当します。
- パターンに
-
matchPackages
関数での利用:matchPackages
関数は、go list
コマンドなどでパッケージを探索する際に使用される主要な関数です。この関数内で、pattern
が"all"
や"std"
でない場合に、matchPattern
に加えてtreeCanMatchPattern
が初期化されます。 ファイルツリーを走査するfilepath.Walk
のコールバック関数内で、treeCanMatch(name)
がfalse
を返した場合、filepath.SkipDir
を返すように変更されました。filepath.SkipDir
を返すと、filepath.Walk
はそのディレクトリの探索をスキップし、そのサブディレクトリにも入らなくなります。
この変更により、例えばgo list m...
というコマンドが実行された場合、treeCanMatchPattern("m...")
はm
で始まらないディレクトリに対してfalse
を返します。これにより、filepath.Walk
はm
で始まらないディレクトリを早期にスキップし、不要なファイルシステムアクセスを削減します。
コアとなるコードの変更箇所
src/cmd/go/main.go
hasPathPrefix
関数の追加treeCanMatchPattern
関数の追加matchPackages
関数内でtreeCanMatchPattern
を初期化し、filepath.Walk
のコールバック内でtreeCanMatch(name)
がfalse
の場合にfilepath.SkipDir
を返すロジックを追加。
--- a/src/cmd/go/main.go
+++ b/src/cmd/go/main.go
@@ -434,6 +434,37 @@ func matchPattern(pattern string) func(name string) bool {
}\n}\n
+
+// hasPathPrefix reports whether the path s begins with the
+// elements in prefix.
+func hasPathPrefix(s, prefix string) bool {
+ switch {
+ default:
+ return false
+ case len(s) == len(prefix):
+ return s == prefix
+ case len(s) > len(prefix):
+ if prefix != "" && prefix[len(prefix)-1] == '/' {
+ return strings.HasPrefix(s, prefix)
+ }
+ return s[len(prefix)] == '/' && s[:len(prefix)] == prefix
+ }
+}
+
+// treeCanMatchPattern(pattern)(name) reports whether
+// name or children of name can possibly match pattern.
+// Pattern is the same limited glob accepted by matchPattern.
+func treeCanMatchPattern(pattern string) func(name string) bool {
+ wildCard := false
+ if i := strings.Index(pattern, "..."); i >= 0 {
+ wildCard = true
+ pattern = pattern[:i]
+ }
+ return func(name string) bool {
+ return len(name) <= len(pattern) && hasPathPrefix(pattern, name) ||
+ wildCard && strings.HasPrefix(name, pattern)
+ }
+}
+
// allPackages returns all the packages that can be found
// under the $GOPATH directories and $GOROOT matching pattern.
// The pattern is either "all" (all packages), "std" (standard packages)
@@ -448,8 +479,10 @@ func allPackages(pattern string) []string {
func matchPackages(pattern string) []string {
match := func(string) bool { return true }
+ treeCanMatch := func(string) bool { return true }
if pattern != "all" && pattern != "std" {
match = matchPattern(pattern)
+ treeCanMatch = treeCanMatchPattern(pattern)
}
have := map[string]bool{
@@ -467,6 +500,9 @@ func matchPackages(pattern string) []string {
return nil
}
name := path[len(cmd):]
+ if !treeCanMatch(name) {
+ return filepath.SkipDir
+ }
// Commands are all in cmd/, not in subdirectories.
if strings.Contains(name, string(filepath.Separator)) {
return filepath.SkipDir
@@ -512,6 +548,9 @@ func matchPackages(pattern string) []string {
if pattern == "std" && strings.Contains(name, ".") {
return filepath.SkipDir
}
+ if !treeCanMatch(name) {
+ return filepath.SkipDir
+ }
if have[name] {
return nil
}
src/cmd/go/match_test.go
TestMatchPattern
のテスト構造体をstringPairTest
型に統一し、testStringPairs
ヘルパー関数を使用するように変更。treeCanMatchPatternTests
という新しいテストケース群と、それらをテストするためのTestChildrenCanMatchPattern
関数を追加。hasPathPrefixTests
という新しいテストケース群と、それらをテストするためのTestHasPathPrefix
関数を追加。stringPairTest
構造体とtestStringPairs
ヘルパー関数を追加。
src/cmd/go/test.bash
- ワイルドカードパターンが不要なディレクトリを探索しないことを確認するための新しいシェルテストケースを追加。このテストは、
badpkg
という存在しないパッケージを含むディレクトリが、m...
のようなパターンで探索されないことを検証します。
src/cmd/go/testdata/src/badpkg/x.go
- テストのために
badpkg
というダミーのパッケージファイルを追加。
コアとなるコードの解説
このコミットの核心は、treeCanMatchPattern
関数と、それがmatchPackages
関数内でfilepath.Walk
の枝刈りにどのように利用されているかです。
treeCanMatchPattern
関数
この関数は、与えられたパターン(例: net...
, net/http
, x/y/z/...
)と、現在走査中のディレクトリ名(name
)を受け取り、そのディレクトリまたはそのサブディレクトリがパターンに合致する可能性があるかどうかを判断します。
- ワイルドカードの処理: パターンに
...
が含まれる場合、wildCard
フラグが立てられ、パターンから...
が取り除かれます。これにより、net...
はnet
として扱われ、net
で始まるすべてのパスにマッチする可能性を考慮します。 - クロージャの返却: 実際のチェックロジックは、返される無名関数(クロージャ)内にカプセル化されています。このクロージャは、
filepath.Walk
によって各ディレクトリが走査されるたびに呼び出されます。 - マッチングロジック:
len(name) <= len(pattern) && hasPathPrefix(pattern, name)
: これは、現在のディレクトリ名name
が、パターンpattern
の「親」または「自身」である場合にtrue
を返します。例えば、pattern="net/http"
でname="net"
の場合、net
ディレクトリはnet/http
を含む可能性があるため、true
を返します。hasPathPrefix
はパスのセパレータを考慮するため、netchan
のようなケースとは区別されます。wildCard && strings.HasPrefix(name, pattern)
: これは、パターンが...
を含むワイルドカードパターン(例:net...
)であり、かつ現在のディレクトリ名name
がそのワイルドカードのプレフィックス(例:net
)で始まる場合にtrue
を返します。これにより、net
で始まるすべてのディレクトリ(net
,net/http
,netchan
など)が探索対象となる可能性を考慮します。
これらの条件のいずれかが満たされれば、そのディレクトリはパターンに合致する可能性があると判断され、探索が続行されます。そうでなければ、そのディレクトリ以下はパターンに合致する可能性がないと判断されます。
matchPackages
関数での利用
matchPackages
関数は、filepath.Walk
を使用してGOPATH
やGOROOT
配下のディレクトリを走査します。この走査中に、各ディレクトリに対してtreeCanMatch
クロージャが呼び出されます。
if !treeCanMatch(name) {
return filepath.SkipDir
}
このコードスニペットが、枝刈りの肝となる部分です。
name
は、現在走査中のディレクトリのパッケージ名(GOPATH
やGOROOT
からの相対パス)です。treeCanMatch(name)
がfalse
を返した場合、つまり、現在のディレクトリname
またはその子孫が、指定されたパッケージパターンに合致する可能性が全くないと判断された場合、filepath.SkipDir
が返されます。filepath.SkipDir
を返すと、filepath.Walk
は現在のディレクトリの残りの処理をスキップし、そのディレクトリのサブディレクトリへの再帰的な探索も行いません。これにより、不要なディレクトリの走査が完全に回避され、ファイルシステムへのアクセスが大幅に削減されます。
この最適化は、特にgo list ...
のように広範囲を探索するコマンドや、GOPATH
に多数のプロジェクトや依存関係が存在する環境において、顕著なパフォーマンス改善をもたらします。
関連リンク
- Go Issue #5214:
go list ...
is very slow: https://code.google.com/p/go/issues/detail?id=5214 (現在はGitHubに移行) - Go CL 13253049:
cmd/go: use pattern to prune file tree walk
: https://golang.org/cl/13253049
参考にした情報源リンク
- Go言語の公式ドキュメント:
go help list
- Go言語の公式ドキュメント:
filepath
パッケージ - Go言語のソースコード:
src/cmd/go/main.go
- Go言語のソースコード:
src/cmd/go/match_test.go
- Go言語のIssueトラッカー: Issue 5214
- Go言語のコードレビューシステム: CL 13253049