[インデックス 14442] ファイルの概要
このコミットは、Go言語のcmd/api
ツールにおけるパフォーマンス改善を目的としています。具体的には、parser.ParseFile
の呼び出し結果をキャッシュすることで、APIチェックの速度を約2倍に向上させています。これにより、開発者のマシンでの実行時間が5秒短縮される効果がありました。この変更は、GoのAST(Abstract Syntax Tree)の深いコピー(クローン)メカニズムを一時的に導入することで実現されており、関連するIssue 4380が修正されれば、このクローン処理は不要になる予定です。
コミット
commit aeca7a7cd21cb512ac19fa2f8d0555fb13bdd8f3
Author: Brad Fitzpatrick <bradfitz@golang.org>
Date: Mon Nov 19 13:50:20 2012 -0800
cmd/api: speed up API check by 2x, caching parser.ParseFile calls
Saves 5 seconds on my machine. If Issue 4380 is fixed this
clone can be removed.
Update #4380
R=golang-dev, remyoudompheng, minux.ma, gri
CC=golang-dev
https://golang.org/cl/6845058
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/aeca7a7cd21cb512ac19fa2f8d0555fb13bdd8f3
元コミット内容
cmd/api: speed up API check by 2x, caching parser.ParseFile calls
Saves 5 seconds on my machine. If Issue 4380 is fixed this
clone can be removed.
Update #4380
変更の背景
このコミットの主な背景は、cmd/api
ツールの実行速度の改善です。cmd/api
は、Goの標準ライブラリのAPI互換性をチェックするためのツールであり、その性質上、多くのGoソースファイルを解析する必要があります。parser.ParseFile
関数は、Goのソースファイルを解析してASTを構築する処理を行いますが、この処理は比較的コストが高いです。
コミットメッセージによると、この変更により開発者のマシンで5秒の高速化が実現され、APIチェックの速度が約2倍になったとされています。これは、開発サイクルにおいて大きな改善となります。
また、この変更は「Issue 4380が修正されれば、このクローンは削除できる」と明記されており、一時的な解決策であることが示唆されています。Issue 4380は、go/parser
パッケージが返すASTノードが、内部的に共有される可能性のある構造を持っていることに関連していると考えられます。もしASTノードが共有されている場合、キャッシュされたASTを直接再利用すると、後続の処理でASTが変更された際に、キャッシュされたASTも意図せず変更されてしまう可能性があります。これを避けるために、キャッシュから取得したASTを深いコピー(クローン)して使用する必要がありました。
前提知識の解説
cmd/api
ツール
cmd/api
は、Go言語の標準ライブラリのAPI互換性を検証するためのツールです。Go言語では、後方互換性が非常に重視されており、新しいバージョンでAPIが変更される際には、既存のコードが壊れないように細心の注意が払われます。cmd/api
は、Goのリリースプロセスの一部として、APIの変更が互換性を損なわないことを確認するために使用されます。具体的には、Goのソースコードを解析し、公開されているAPI(エクスポートされた型、関数、変数など)のシグネチャや構造を抽出し、以前のバージョンと比較することで、互換性のない変更がないかを検出します。
go/ast
パッケージ
go/ast
パッケージは、Go言語のソースコードの抽象構文木(Abstract Syntax Tree, AST)を表現するためのデータ構造を提供します。ASTは、プログラムのソースコードを木構造で表現したもので、コンパイラやリンター、コード分析ツールなどがソースコードを理解し、操作するために利用します。go/ast
パッケージには、ファイル、宣言、式、ステートメントなどのGo言語の構文要素に対応する様々な構造体(例: ast.File
, ast.FuncDecl
, ast.Expr
など)が定義されています。
go/parser
パッケージ
go/parser
パッケージは、Go言語のソースコードを解析し、go/ast
パッケージで定義されたASTを構築するための機能を提供します。このパッケージの主要な関数の一つがparser.ParseFile
です。parser.ParseFile
は、指定されたGoソースファイルのパスを読み込み、その内容を解析して*ast.File
型のASTルートノードを返します。このASTは、その後のコード分析や変換の基盤となります。
ASTの深いコピー(Deep Copy)と浅いコピー(Shallow Copy)
- 浅いコピー(Shallow Copy): オブジェクトの最上位の構造のみをコピーし、そのオブジェクトが参照している他のオブジェクトはコピーせず、元のオブジェクトと同じ参照を共有します。つまり、コピーされたオブジェクトと元のオブジェクトが同じ内部オブジェクトを指すため、一方のオブジェクトの内部オブジェクトを変更すると、もう一方のオブジェクトにも影響が及びます。
- 深いコピー(Deep Copy): オブジェクトとそのオブジェクトが参照しているすべてのオブジェクトを再帰的にコピーします。これにより、コピーされたオブジェクトは元のオブジェクトとは完全に独立した新しいオブジェクトツリーを持つため、一方のオブジェクトを変更してももう一方には影響しません。
このコミットでは、parser.ParseFile
が返す*ast.File
オブジェクトが、内部的に共有される可能性のある構造を持っているため、キャッシュから取得したASTを安全に利用するために深いコピー(クローン)が必要とされました。
Issue 4380
GoのIssue 4380は、「go/parser
が返すASTノードは、go/token.FileSet
の内部構造を共有しているため、go/token.FileSet
が変更されるとASTノードも変更される可能性がある」という問題に関連しています。この問題が修正されれば、parser.ParseFile
が返すASTは完全に独立したものとなり、深いコピーを行う必要がなくなると考えられます。
技術的詳細
このコミットの技術的な核心は、cmd/api
ツールがGoソースファイルを解析する際のパフォーマンスボトルネックを解消するために、ASTのキャッシュと深いコピー(クローン)を導入した点にあります。
-
parser.ParseFile
のキャッシュ化:goapi.go
にparsedFileCache
というmap[string]*ast.File
型のグローバル変数が追加されました。これは、ファイルパスをキーとして、解析済みの*ast.File
オブジェクトを保存するためのキャッシュです。parseFile
という新しいヘルパー関数が導入されました。この関数は、まずparsedFileCache
に指定されたファイルパスのASTが存在するかどうかを確認します。- もしキャッシュに存在すれば、そのキャッシュされたASTを返します。
- キャッシュに存在しない場合は、
parser.ParseFile
を呼び出してファイルを解析し、結果をキャッシュに保存してから返します。
-
ASTの深いコピー(クローン):
- キャッシュされたASTを直接返すと、その後の処理でASTが変更された場合に、キャッシュされたASTも変更されてしまい、予期せぬ副作用を引き起こす可能性があります。これを防ぐため、キャッシュからASTを取得する際に、
clone
関数を使用してASTの深いコピーを作成しています。 clone.go
という新しいファイルが追加され、clone
関数が定義されています。この関数は、interface{}
を受け取り、GoのASTパッケージで定義されている様々なASTノード型(*ast.File
,*ast.GenDecl
,*ast.FuncType
など)に対応するswitch
文を持ち、それぞれの型に対して再帰的に深いコピーを作成します。clone
関数は、reflect.DeepEqual
を用いたデバッグ用のチェックも含まれており、クローンが元のオブジェクトと等しいことを検証できます(debugClone
がtrue
の場合)。
- キャッシュされたASTを直接返すと、その後の処理でASTが変更された場合に、キャッシュされたASTも変更されてしまい、予期せぬ副作用を引き起こす可能性があります。これを防ぐため、キャッシュからASTを取得する際に、
-
cmd/api
の既存コードの変更:src/cmd/api/goapi.go
のWalkPackage
関数内で、ファイルの解析に直接parser.ParseFile
を呼び出す代わりに、新しく導入されたparseFile
ヘルパー関数を使用するように変更されました。これにより、ファイルの解析結果がキャッシュされるようになります。
このアプローチにより、同じファイルを複数回解析する必要がある場合に、2回目以降の解析ではキャッシュからASTを取得し、その深いコピーを使用することで、parser.ParseFile
のコストの高い処理をスキップできるようになり、全体的なパフォーマンスが向上します。
コアとなるコードの変更箇所
src/cmd/api/clone.go
(新規ファイル)
// Copyright 2012 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.
package main
import (
"fmt"
"go/ast"
"log"
"reflect"
)
const debugClone = false
// TODO(bradfitz): delete this function (and whole file) once
// http://golang.org/issue/4380 is fixed.
func clone(i interface{}) (cloned interface{}) {
if debugClone {
defer func() {
if !reflect.DeepEqual(i, cloned) {
log.Printf("cloned %T doesn't match: in=%#v out=%#v", i, i, cloned)
}
}()
}
switch v := i.(type) {
case nil:
return nil
case *ast.File:
o := &ast.File{
Doc: v.Doc, // shallow
Package: v.Package,
Comments: v.Comments, // shallow
Name: v.Name,
Scope: v.Scope,
}
for _, x := range v.Decls {
o.Decls = append(o.Decls, clone(x).(ast.Decl))
}
for _, x := range v.Imports {
o.Imports = append(o.Imports, clone(x).(*ast.ImportSpec))
}
for _, x := range v.Unresolved {
o.Unresolved = append(o.Unresolved, x)
}
return o
// ... (他のastノード型のクローン処理が続く)
case *ast.Ident, *ast.BasicLit:
return v
// ... (他のastノード型のクローン処理が続く)
}
panic(fmt.Sprintf("Uncloneable type %T", i))
}
func cloneExpr(x ast.Expr) ast.Expr {
if x == nil {
return nil
}
return clone(x).(ast.Expr)
}
func cloneExprs(x []ast.Expr) []ast.Expr {
if x == nil {
return nil
}
o := make([]ast.Expr, len(x))
for i, x := range x {
o[i] = cloneExpr(x)
}
return o
}
src/cmd/api/goapi.go
--- a/src/cmd/api/goapi.go
+++ b/src/cmd/api/goapi.go
@@ -353,6 +353,21 @@ func fileDeps(f *ast.File) (pkgs []string) {
return
}
+var parsedFileCache = make(map[string]*ast.File)
+
+func parseFile(filename string) (*ast.File, error) {
+ f, ok := parsedFileCache[filename]
+ if !ok {
+ var err error
+ f, err = parser.ParseFile(fset, filename, nil, 0)
+ if err != nil {
+ return nil, err
+ }
+ parsedFileCache[filename] = f
+ }
+ return clone(f).(*ast.File), nil
+}
+
// WalkPackage walks all files in package `name\'.
// WalkPackage does nothing if the package has already been loaded.
func (w *Walker) WalkPackage(name string) {
@@ -386,7 +401,7 @@ func (w *Walker) WalkPackage(name string) {
files := append(append([]string{}, info.GoFiles...), info.CgoFiles...)\n for _, file := range files {\n-\t\tf, err := parser.ParseFile(fset, filepath.Join(dir, file), nil, 0)\n+\t\tf, err := parseFile(filepath.Join(dir, file))\n \t\tif err != nil {\n \t\t\tlog.Fatalf(\"error parsing package %s, file %s: %v\", name, file, err)\n \t\t}\n```
### `src/cmd/api/testdata/src/pkg/p1/p1.go`
このファイルはテストデータであり、新しいASTノードタイプ(`...string`, `&S{}`, `(1+5)`, `func(){}`, `map[string]int`, `chan int`, `interface{}`, `ifaceVar.(int)`, `m["foo"]`)が`clone`関数で正しく処理されることを確認するために追加されたものです。
```diff
--- a/src/cmd/api/testdata/src/pkg/p1/p1.go
+++ b/src/cmd/api/testdata/src/pkg/p1/p1.go
@@ -167,3 +167,25 @@ const (
foo2 string = "foo2"
truth = foo == "foo" || foo2 == "foo2"
)
+
+func ellipsis(...string) {}
+
+var x = &S{
+ Public: nil,
+ private: nil,
+ publicTime: time.Now(),
+}
+
+var parenExpr = (1 + 5)
+
+var funcLit = func() {}
+
+var m map[string]int
+
+var chanVar chan int
+
+var ifaceVar interface{} = 5
+
+var assertVar = ifaceVar.(int)
+
+var indexVar = m["foo"]
コアとなるコードの解説
clone.go
このファイルは、GoのASTノードを深いコピーするためのclone
関数を定義しています。
clone(i interface{}) (cloned interface{})
:- この関数は、
interface{}
型の引数i
を受け取り、その深いコピーを返します。 debugClone
がtrue
の場合、reflect.DeepEqual
を使ってクローンが元のオブジェクトと等しいかどうかの検証を行います。これはデバッグ目的で、クローン処理が正しく行われているかを確認するために使用されます。switch v := i.(type)
文を使って、入力されたinterface{}
の具体的な型を判別します。- 各
case
ブロックでは、対応するast
パッケージの構造体(例:*ast.File
,*ast.GenDecl
,*ast.FuncType
など)に対して、新しいインスタンスを作成し、そのフィールドを再帰的にクローンしていきます。 - 例えば、
*ast.File
の場合、Doc
,Package
,Comments
,Name
,Scope
などのフィールドは浅いコピー(または直接代入)されますが、Decls
,Imports
,Unresolved
などのスライスやポインタを含むフィールドは、clone
関数を再帰的に呼び出して深いコピーを作成しています。 *ast.Ident
や*ast.BasicLit
のようなプリミティブなASTノード(それ以上内部にポインタを持たないもの)は、直接値を返しています。- 対応する型が見つからない場合は
panic
を発生させ、クローンできない型であることを示します。
- この関数は、
cloneExpr(x ast.Expr) ast.Expr
:ast.Expr
型の式をクローンするためのヘルパー関数です。clone
関数を呼び出し、結果をast.Expr
型にキャストして返します。
cloneExprs(x []ast.Expr) []ast.Expr
:ast.Expr
型のスライスの要素をそれぞれクローンするためのヘルパー関数です。新しいスライスを作成し、各要素に対してcloneExpr
を呼び出して深いコピーを作成します。
このclone
関数は、go/parser
が返すASTが内部的に共有される可能性のある構造を持っているため、キャッシュから取得したASTを安全に操作するために必要とされました。Issue 4380が解決されれば、このファイル全体が削除される予定です。
goapi.go
このファイルは、cmd/api
ツールの主要なロジックを含んでいます。
var parsedFileCache = make(map[string]*ast.File)
:- ファイルパスをキーとし、解析済みの
*ast.File
を値とするマップです。これがASTのキャッシュとして機能します。
- ファイルパスをキーとし、解析済みの
func parseFile(filename string) (*ast.File, error)
:- この新しい関数は、ファイルの解析処理をカプセル化し、キャッシュロジックを追加します。
- まず、
parsedFileCache
にfilename
が存在するかどうかを確認します。 ok
がtrue
(キャッシュヒット)の場合、キャッシュされたf
をclone(f).(*ast.File)
として深いコピーを作成し、それを返します。これにより、キャッシュされたASTが後続の処理で変更されることを防ぎます。ok
がfalse
(キャッシュミス)の場合、parser.ParseFile
を呼び出してファイルを実際に解析します。- 解析が成功した場合、結果の
f
をparsedFileCache
に保存し、そのf
の深いコピーを返します。
func (w *Walker) WalkPackage(name string)
内の変更:- 以前は
parser.ParseFile(fset, filepath.Join(dir, file), nil, 0)
を直接呼び出していましたが、このコミットでparseFile(filepath.Join(dir, file))
に置き換えられました。 - これにより、
WalkPackage
がファイルを解析する際に、キャッシュが利用され、パフォーマンスが向上します。
- 以前は
この変更により、cmd/api
ツールが同じGoソースファイルを複数回解析する必要がある場合に、2回目以降の解析ではキャッシュからASTを取得し、その深いコピーを使用することで、parser.ParseFile
の実行をスキップできるようになり、全体的な処理時間が大幅に短縮されます。
関連リンク
- GitHubコミットページ: https://github.com/golang/go/commit/aeca7a7cd21cb512ac19fa2f8d0555fb13bdd8f3
- Go Issue 4380: https://golang.org/issue/4380
- Go CL 6845058: https://golang.org/cl/6845058
参考にした情報源リンク
- Go言語のAST (Abstract Syntax Tree) について:
- Go言語の
cmd/api
ツールについて:- https://go.dev/doc/go1.0#api (Go 1.0のAPI互換性に関する記述)
- Go言語における深いコピーと浅いコピーの概念:
- https://go.dev/blog/go-slices-usage-and-internals (スライスに関する記事だが、コピーの概念に触れている)
- Go言語のIssueトラッカー:
- Go Code Review Comments (CL):
reflect.DeepEqual
について: