[インデックス 14840] ファイルの概要
このコミットは、Go言語の型チェッカー (go/types パッケージ) において、インポートされたパッケージが複数回パースされるのを防ぐための変更です。これにより、型チェックの効率が向上し、冗長な処理が削減されます。
コミット
このコミットは、Go言語の標準ライブラリの一部である go/types パッケージ内の check.go ファイルに対する修正です。主な目的は、型チェックの過程でインポートされるパッケージが不必要に複数回解析されることを防ぐことです。具体的には、GcImport 関数をラップし、既にインポートされたパッケージのパスを追跡する imported マップを導入することで、重複する解析処理をスキップするように変更されています。これにより、大規模なプロジェクトや多数の依存関係を持つコードベースでの型チェックのパフォーマンスが改善されることが期待されます。
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/e0bf0374ca11e4a51315500a5b08cda492eb715b
元コミット内容
commit e0bf0374ca11e4a51315500a5b08cda492eb715b
Author: Rémy Oudompheng <oudomphe@phare.normalesup.org>
Date: Wed Jan 9 22:03:41 2013 +0100
go/types: don't parse imported packages multiple times.
R=dave, golang-dev, rsc
CC=golang-dev
https://golang.org/cl/7068044
---
src/pkg/go/types/check.go | 13 ++++++++++++-
1 file changed, 12 insertions(+), 1 deletion(-)
diff --git a/src/pkg/go/types/check.go b/src/pkg/go/types/check.go
index 10b67bcab9..cebba7abf5 100644
--- a/src/pkg/go/types/check.go
+++ b/src/pkg/go/types/check.go
@@ -393,7 +393,18 @@ func check(ctxt *Context, fset *token.FileSet, files map[string]*ast.File) (pkg
// resolve identifiers
imp := ctxt.Import
if imp == nil {
-\t\timp = GcImport
+\t\t// wrap GcImport to import packages only once by default.
+\t\timported := make(map[string]bool)
+\t\timp = func(imports map[string]*ast.Object, path string) (*ast.Object, error) {
+\t\t\tif imported[path] && imports[path] != nil {\
+\t\t\t\treturn imports[path], nil
+\t\t\t}\
+\t\t\tpkg, err := GcImport(imports, path)\
+\t\t\tif err == nil {\
+\t\t\t\timported[path] = true
+\t\t\t}\
+\t\t\treturn pkg, err
+\t\t}
}
pkg, err = ast.NewPackage(fset, files, imp, Universe)\
if err != nil {\
変更の背景
Go言語のコンパイラツールチェーンの一部である go/types パッケージは、Goソースコードの型チェックを担当します。型チェックのプロセスでは、ソースファイルが依存する他のパッケージをインポートし、その型情報を解決する必要があります。
このコミットがなされた背景には、型チェックの効率化という課題がありました。従来の GcImport 関数を直接使用する実装では、同じパッケージが複数の場所からインポートされる場合、そのパッケージが複数回パース(解析)される可能性がありました。パッケージのパースはI/O操作やAST(抽象構文木)構築を伴う比較的コストの高い処理であり、これが重複して実行されると、特に大規模なコードベースや多数の依存関係を持つプロジェクトにおいて、型チェックの時間が不必要に長くなる原因となります。
この非効率性を解消し、型チェックのパフォーマンスを向上させるために、インポートされたパッケージの重複パースを回避するメカニズムが必要とされました。このコミットは、GcImport をラップし、既に処理済みのパッケージを記憶することで、この問題を解決しようとするものです。
前提知識の解説
このコミットを理解するためには、以下のGo言語のツールチェーンと型システムに関する基本的な知識が必要です。
-
go/typesパッケージ: Go言語の標準ライブラリの一部であり、Goプログラムの型チェックを行うためのAPIを提供します。これは、コンパイラ、リンター、IDEなどのツールがGoコードのセマンティックな解析を行う際に利用されます。go/typesは、Goの仕様に厳密に従って、式の型、変数のスコープ、関数のシグネチャなどを検証します。 -
ast(Abstract Syntax Tree) パッケージ: Goソースコードの抽象構文木(AST)を表現するためのデータ構造と関数を提供します。Goのソースコードは、まず字句解析と構文解析を経てASTに変換されます。型チェックやコード分析は、このASTを走査することで行われます。 -
tokenパッケージ: Goソースコード内のトークン(キーワード、識別子、演算子など)の位置情報(ファイル、行、列)を管理するためのパッケージです。token.FileSetは、複数のソースファイルにまたがる位置情報を効率的に管理するために使用されます。 -
ast.NewPackage関数:go/typesパッケージ内で使用される関数で、与えられたASTファイルセットからパッケージの型情報を構築します。この関数は、パッケージのインポート解決のためにimp(import) 関数を引数として受け取ります。 -
GcImport関数: Goコンパイラ(gc)が生成するバイナリ形式のパッケージ情報(.aファイルなど)を読み込み、その型情報をgo/typesが扱える形式に変換する関数です。これは、Goのビルドシステムが依存関係を解決し、既にコンパイルされたパッケージの型情報を利用する際に重要な役割を果たします。 -
パッケージのインポートと型チェックのプロセス: Goのビルドプロセスでは、ソースファイルが
importステートメントを通じて他のパッケージを参照します。型チェッカーは、これらのインポートされたパッケージの型定義を解決し、現在のパッケージのコードがそれらを正しく使用しているか検証します。この解決プロセスには、インポートされたパッケージのソースコードをパースしたり、コンパイル済みのパッケージ情報を読み込んだりする作業が含まれます。
技術的詳細
このコミットの技術的な核心は、go/types パッケージの check.go ファイル内の check 関数における imp (import) 関数の挙動の変更にあります。
check 関数は、Goパッケージの型チェックを行う主要なエントリポイントの一つです。この関数内で、imp という関数型の変数が定義されており、これはパッケージのインポート処理を担当します。
元のコードでは、ctxt.Import が nil の場合、デフォルトのインポート関数として GcImport が直接割り当てられていました。GcImport は、指定されたパスのパッケージを読み込み、その型情報を返します。しかし、この GcImport は、呼び出されるたびにパッケージを新たにパースしようとするため、同じパッケージが複数回インポートされるシナリオでは、その都度パース処理が実行され、非効率的でした。
このコミットでは、この問題を解決するために、GcImport を直接割り当てる代わりに、匿名関数で GcImport をラップしています。このラッパー関数は以下のロジックを含んでいます。
-
importedマップの導入:imported := make(map[string]bool)という行で、string(パッケージパス) をキーとし、boolを値とするマップimportedが導入されます。このマップは、既に正常にインポートされたパッケージのパスを記録するために使用されます。 -
重複インポートのチェック:
if imported[path] && imports[path] != nilという条件が追加されました。imported[path]は、このパッケージパスが以前に正常にインポートされたことがあるかを確認します。imports[path] != nilは、importsマップ(ast.NewPackageに渡される、既に解決されたオブジェクトを保持するマップ)にそのパッケージパスのエントリが既に存在するかを確認します。 この両方が真である場合、つまり、そのパッケージが以前に正常にインポートされ、かつimportsマップにもその情報が既に存在する場合は、新たなパース処理を行うことなく、既存のimports[path]の値をそのまま返します。これにより、重複するパース処理が回避されます。
-
GcImportの呼び出しと記録: 上記の条件が偽の場合(つまり、まだインポートされていないか、情報が不完全な場合)、pkg, err := GcImport(imports, path)を呼び出して実際のパッケージインポート処理を実行します。 インポートがエラーなく成功した場合 (if err == nil)、imported[path] = trueを設定し、このパッケージパスが正常に処理されたことをimportedマップに記録します。
この変更により、go/types はインポートされたパッケージを一度だけパースし、その結果をキャッシュするような挙動を実現します。これにより、型チェックのパフォーマンスが向上し、特に依存関係の多いプロジェクトでのビルド時間が短縮される効果が期待できます。
コアとなるコードの変更箇所
--- a/src/pkg/go/types/check.go
+++ b/src/pkg/go/types/check.go
@@ -393,7 +393,18 @@ func check(ctxt *Context, fset *token.FileSet, files map[string]*ast.File) (pkg
// resolve identifiers
imp := ctxt.Import
if imp == nil {
-\t\timp = GcImport
+\t\t// wrap GcImport to import packages only once by default.
+\t\timported := make(map[string]bool)
+\t\timp = func(imports map[string]*ast.Object, path string) (*ast.Object, error) {
+\t\t\tif imported[path] && imports[path] != nil {\
+\t\t\t\treturn imports[path], nil
+\t\t\t}\
+\t\t\tpkg, err := GcImport(imports, path)\
+\t\t\tif err == nil {\
+\t\t\t\timported[path] = true
+\t\t\t}\
+\t\t\treturn pkg, err
+\t\t}
}
pkg, err = ast.NewPackage(fset, files, imp, Universe)\
if err != nil {\
コアとなるコードの解説
変更は src/pkg/go/types/check.go ファイルの check 関数内、imp 変数の初期化部分に集中しています。
-
変更前:
imp := ctxt.Import if imp == nil { imp = GcImport }ctxt.Importが設定されていない場合、impは直接GcImport関数に設定されていました。これは、GcImportが呼び出されるたびにパッケージのパースを試みることを意味します。 -
変更後:
imp := ctxt.Import if imp == nil { // wrap GcImport to import packages only once by default. imported := make(map[string]bool) imp = func(imports map[string]*ast.Object, path string) (*ast.Object, error) { if imported[path] && imports[path] != nil { return imports[path], nil } pkg, err := GcImport(imports, path) if err == nil { imported[path] = true } return pkg, err } }ctxt.Importがnilの場合、impには新しい匿名関数が割り当てられます。imported := make(map[string]bool): この匿名関数がクロージャとして利用するローカル変数importedマップが定義されます。このマップは、既に正常にインポートされたパッケージのパスを記憶するために使用されます。このマップはimp関数が作成されるときに一度だけ初期化され、その後のimpの呼び出し間で状態を保持します。imp = func(imports map[string]*ast.Object, path string) (*ast.Object, error) { ... }: これが新しいインポート処理のロジックです。if imported[path] && imports[path] != nil: この条件は、現在のpathのパッケージが既にimportedマップに記録されており、かつimportsマップにもそのオブジェクトが既に存在する場合に真となります。return imports[path], nil: 上記の条件が真の場合、GcImportを呼び出すことなく、既に解決済みのパッケージオブジェクトを返します。これにより、重複するパース処理が回避されます。pkg, err := GcImport(imports, path): パッケージがまだインポートされていないか、情報が不完全な場合は、実際のGcImport関数が呼び出され、パッケージのパースと型情報の取得が行われます。if err == nil { imported[path] = true }:GcImportがエラーなく成功した場合、そのパッケージパスをimportedマップに記録し、次回以降の重複パースを防ぎます。
この変更により、go/types パッケージは、デフォルトでインポートされたパッケージを一度だけ処理するようになり、型チェックの効率が大幅に向上します。
関連リンク
- Gerrit Change-Id:
https://golang.org/cl/7068044このコミットの元のGerritレビューページです。詳細な議論や関連する変更履歴を確認できます。
参考にした情報源リンク
- Go言語の公式ドキュメント (
go/types,go/ast,go/tokenパッケージ): - Go言語のコンパイラとツールチェーンに関する一般的な情報源。
- Go言語の型システムに関する解説記事。
- Go言語のソースコード (
src/pkg/go/types/check.go)。- https://github.com/golang/go/blob/master/src/go/types/check.go (コミット当時のコードとは異なる可能性がありますが、現在の構造を理解するのに役立ちます。)