[インデックス 14620] ファイルの概要
このコミットは、Goコンパイラ(cmd/gc
)が同じパッケージの情報を複数回インポートすることを防ぐための変更です。これにより、ビルド時間の短縮とメモリ使用量の削減が実現されました。
コミット
commit 11999306df8b02dbbf26ac0772429c8ca5754ab5
Author: Rémy Oudompheng <oudomphe@phare.normalesup.org>
Date: Wed Dec 12 08:47:09 2012 +0100
cmd/gc: don't import the same package multiple times.
Implementation suggested by DMorsing.
R=golang-dev, dave, daniel.morsing, rsc
CC=golang-dev
https://golang.org/cl/6903059
---
src/cmd/gc/go.h | 1 +\
src/cmd/gc/lex.c | 10 ++++++++++\
2 files changed, 11 insertions(+)
diff --git a/src/cmd/gc/go.h b/src/cmd/gc/go.h
index 0280c965c9..36bc4b2954 100644
--- a/src/cmd/gc/go.h
+++ b/src/cmd/gc/go.h
@@ -388,6 +388,7 @@ struct Pkg
Sym* pathsym;
char* prefix; // escaped path for use in symbol table
Pkg* link;
+ uchar imported; // export data of this package was parsed
char exported; // import line written in export data
char direct; // imported directly
};
diff --git a/src/cmd/gc/lex.c b/src/cmd/gc/lex.c
index 6481ceb1e1..eabeaeb646 100644
--- a/src/cmd/gc/lex.c
+++ b/src/cmd/gc/lex.c
@@ -690,6 +690,16 @@ importfile(Val *f, int line)\n }\n importpkg = mkpkg(path);\n \n+\t// If we already saw that package, feed a dummy statement\n+\t// to the lexer to avoid parsing export data twice.\n+\tif(importpkg->imported) {\n+\t\tfile = strdup(namebuf);\n+\t\tp = smprint(\"package %s\\n$$\\n\", importpkg->name);\n+\t\tcannedimports(file, p);\n+\t\treturn;\n+\t}\n+\timportpkg->imported = 1;\n+\n \timp = Bopen(namebuf, OREAD);\n \tif(imp == nil) {\n \t\tyyerror(\"can\'t open import: \\\"%Z\\\": %r\", f->u.sval);\n```
## GitHub上でのコミットページへのリンク
[https://github.com/golang/go/commit/11999306df8b02dbbf26ac0772429c8ca5754ab5](https://github.com/golang/go/commit/11999306df8b02dbbf26ac0772429c8ca5754ab5)
## 元コミット内容
このコミットは、Goコンパイラのフロントエンド部分である`cmd/gc`において、同じパッケージのインポート処理が複数回行われるのを防ぐことを目的としています。具体的には、一度解析されたパッケージのエクスポートデータ(他のパッケージから参照される型や関数の情報)を再度解析しないようにすることで、コンパイルの効率を向上させます。この実装は、DMorsing氏によって提案されたアイデアに基づいています。
## 変更の背景
Goコンパイラは、ソースコードを解析し、実行可能なバイナリやライブラリを生成する役割を担っています。このプロセスにおいて、他のパッケージで定義されたシンボル(変数、関数、型など)を参照するために、それらのパッケージのエクスポートデータを読み込む必要があります。
しかし、このコミットが導入される以前は、Goコンパイラが同じパッケージのエクスポートデータを複数回読み込み、解析してしまうという非効率な挙動がありました。これは特に、多くのパッケージが相互に依存している大規模なプロジェクトや、標準ライブラリ全体をビルドする際に顕著な問題となっていました。
具体的には、`go build -a std`(標準ライブラリ全体を再ビルドするコマンド)を実行した際に、ビルド時間が長くなり、コンパイラが消費するメモリ量も増大するという課題がありました。この重複した処理は、ディスクI/Oの増加や、メモリ上での重複したデータ構造の構築を引き起こし、コンパイル性能のボトルネックとなっていました。
この問題を解決するために、一度インポートされたパッケージのエクスポートデータは再解析しないようにする、という最適化が提案され、このコミットで実装されました。これにより、ビルド時間の短縮とメモリ使用量の削減という、コンパイラの全体的なパフォーマンス向上に貢献しました。
## 前提知識の解説
* **Goコンパイラ (`cmd/gc`)**: Go言語の公式コンパイラです。Goのソースコードを機械語に変換する主要なツールであり、`src/cmd/gc`ディレクトリにそのソースコードが格納されています。
* **パッケージインポート**: Go言語では、`import`キーワードを使用して他のパッケージの機能を利用します。コンパイラは、`import`文を処理する際に、インポートされるパッケージのエクスポートデータ(そのパッケージが外部に公開している情報)を読み込みます。
* **エクスポートデータ**: Goのパッケージがコンパイルされると、そのパッケージが外部に公開する型、関数、変数などの情報が「エクスポートデータ」として生成されます。他のパッケージがそのパッケージをインポートする際に、このエクスポートデータが読み込まれ、シンボル解決などに利用されます。
* **`src/cmd/gc/go.h`**: Goコンパイラの内部で使用されるヘッダーファイルの一つで、データ構造の定義やグローバルな設定などが含まれています。このコミットでは、`Pkg`構造体に新しいフィールドが追加されています。
* **`src/cmd/gc/lex.c`**: Goコンパイラの字句解析(lexer)と一部の構文解析(parser)に関連する処理が含まれるC言語のソースファイルです。特に、`importfile`関数はパッケージのインポート処理を担当しています。
* **`Pkg`構造体**: Goコンパイラ内部でパッケージの情報を表現するためのデータ構造です。パッケージ名、パス、シンボルテーブルなどの情報が含まれます。
## 技術的詳細
このコミットの技術的な核心は、Goコンパイラがパッケージのエクスポートデータを読み込む際に、そのパッケージが既に処理済みであるかどうかを効率的に判断し、重複処理を回避するメカニズムを導入した点にあります。
以前のコンパイラでは、`import`文が見つかるたびに、対応するパッケージのエクスポートデータをディスクから読み込み、解析しようとしていました。たとえ同じパッケージが複数の場所からインポートされていても、この処理が繰り返されていました。
このコミットでは、`Pkg`構造体に`imported`という新しいフィールド(`uchar`型)を追加しました。このフィールドは、特定のパッケージのエクスポートデータが既に解析されたかどうかを示すフラグとして機能します。
`src/cmd/gc/lex.c`内の`importfile`関数は、パッケージのインポート処理の入り口です。この関数が呼び出された際に、まずインポートしようとしているパッケージに対応する`Pkg`構造体の`imported`フラグをチェックします。
* もし`imported`フラグが既に`1`(真)であれば、そのパッケージのエクスポートデータは既に解析済みであることを意味します。この場合、実際のファイル読み込みや解析処理をスキップし、字句解析器に対してダミーのステートメント(`package %s\n$$\n`)をフィードします。これにより、コンパイラはあたかもパッケージが正常にインポートされたかのように振る舞いますが、実際の重い処理は行われません。
* もし`imported`フラグが`0`(偽)であれば、そのパッケージはまだ解析されていないため、通常通りエクスポートデータを読み込み、解析処理を進めます。そして、解析が完了した後に`imported`フラグを`1`に設定します。
このシンプルなフラグ管理により、コンパイラは同じパッケージのエクスポートデータをディスクから複数回読み込んだり、メモリ上で重複して解析したりする無駄を排除できるようになりました。結果として、ディスクI/Oの削減、CPU使用率の低下、そしてメモリ使用量の最適化が実現され、特に大規模なGoプロジェクトのコンパイル時間が大幅に改善されました。
具体的な性能改善としては、`go build -a std`のビルド時間が1分9.945秒から59.224秒へと短縮され、`pkg/net`のコンパイル時のメモリ使用量が95MBから43MBへと半減したことが報告されています。
## コアとなるコードの変更箇所
このコミットによる主要なコード変更は以下の2つのファイルに集中しています。
1. **`src/cmd/gc/go.h`**:
* `struct Pkg`定義に新しいフィールド `uchar imported;` が追加されました。このフィールドは、そのパッケージのエクスポートデータが既に解析されたかどうかを示すフラグとして使用されます。
```diff
--- a/src/cmd/gc/go.h
+++ b/src/cmd/gc/go.h
@@ -388,6 +388,7 @@ struct Pkg
Sym* pathsym;
char* prefix; // escaped path for use in symbol table
Pkg* link;
+ uchar imported; // export data of this package was parsed
char exported; // import line written in export data
char direct; // imported directly
};
```
2. **`src/cmd/gc/lex.c`**:
* `importfile`関数内に、パッケージが既にインポート済みであるかをチェックし、重複解析を回避するためのロジックが追加されました。
* `importpkg->imported`フラグが`1`の場合、ダミーの`package`ステートメントを生成し、`cannedimports`関数に渡して処理をスキップします。
* パッケージがまだインポートされていない場合(`imported`フラグが`0`)、通常のインポート処理の前に`importpkg->imported = 1;`を設定します。
```diff
--- a/src/cmd/gc/lex.c
+++ b/src/cmd/gc/lex.c
@@ -690,6 +690,16 @@ importfile(Val *f, int int line)\n }\n importpkg = mkpkg(path);\n \n+\t// If we already saw that package, feed a dummy statement\n+\t// to the lexer to avoid parsing export data twice.\n+\tif(importpkg->imported) {\n+\t\tfile = strdup(namebuf);\n+\t\tp = smprint(\"package %s\\n$$\\n\", importpkg->name);\n+\t\tcannedimports(file, p);\n+\t\treturn;\n+\t}\n+\timportpkg->imported = 1;\n+\n \timp = Bopen(namebuf, OREAD);\n \tif(imp == nil) {\n \t\tyyerror(\"can\'t open import: \\\"%Z\\\": %r\", f->u.sval);\
```
## コアとなるコードの解説
* **`src/cmd/gc/go.h` の変更**:
* `Pkg`構造体は、Goコンパイラが各パッケージに関するメタデータを管理するために使用する中心的なデータ構造です。この構造体に`imported`という1バイトの符号なし文字(`uchar`)フィールドが追加されました。このフィールドは、そのパッケージのエクスポートデータが既に読み込まれ、解析されたかどうかを示すブーリアンフラグとして機能します。`0`は未解析、`1`は解析済みを示します。これにより、コンパイラは各パッケージの解析状態を追跡できるようになります。
* **`src/cmd/gc/lex.c` の `importfile` 関数の変更**:
* `importfile`関数は、Goソースコード内の`import`文を処理し、対応するパッケージのエクスポートデータを読み込む役割を担っています。
* **重複チェックとスキップロジック**:
```c
if(importpkg->imported) {
file = strdup(namebuf);
p = smprint("package %s\\n$$\\n", importpkg->name);
cannedimports(file, p);
return;
}
```
このブロックが追加されたことで、`importfile`が呼び出された際に、まず`importpkg->imported`フラグがチェックされます。もしこのフラグが`1`であれば、そのパッケージは既に処理済みであるため、実際のファイルI/Oやエクスポートデータの解析といった重い処理は完全にスキップされます。代わりに、`cannedimports`関数にダミーの文字列(`package <package_name>\n$$\n`)が渡されます。このダミー文字列は、字句解析器に対して、あたかもパッケージが正常にインポートされたかのように見せかけるためのもので、実際の意味のある処理は行われません。これにより、コンパイルのオーバーヘッドが大幅に削減されます。
* **フラグの設定**:
```c
importpkg->imported = 1;
```
この行は、実際のパッケージのエクスポートデータが読み込まれる直前に配置されています。これにより、パッケージが初めてインポートされ、そのエクスポートデータが解析される際に、`imported`フラグが`1`に設定されます。この設定によって、以降同じパッケージが再度インポートされようとした場合には、上記の重複チェックロジックによって処理がスキップされるようになります。
これらの変更により、Goコンパイラはパッケージのインポート処理をより効率的に行い、特に多数のパッケージを持つ大規模なGoプロジェクトのビルド時間を短縮し、メモリ使用量を最適化することに成功しました。
## 関連リンク
* Go CL 6903059: [https://golang.org/cl/6903059](https://golang.org/cl/6903059)
## 参考にした情報源リンク
* Go CL 6903059 のレビューコメントと詳細情報 (web_fetchで取得した情報)