[インデックス 11233] ファイルの概要
このコミットは、Go言語の標準ライブラリの一部である go/token
パッケージにおける FileSet
のファイルイテレーションメカニズムの変更に関するものです。具体的には、既存のチャネルベースのイテレータ Files()
を、より高速なクロージャベースのイテレータ Iterate()
に置き換えることで、パフォーマンスの向上とコードの整理を図っています。
コミット
commit 9edabbe03832e1203d0819c27542b6316ca39d0d
Author: Robert Griesemer <gri@golang.org>
Date: Wed Jan 18 14:10:42 2012 -0800
go/token: replaced Files() with Iterate()
- Use a faster closure-based iterator rather than a channel-based one.
- Otherwise: better code organization, but no other API changes.
R=r, r
CC=golang-dev
https://golang.org/cl/5557051
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/9edabbe03832e1203d0819c27542b6316ca39d0d
元コミット内容
go/token: replaced Files() with Iterate()
- チャネルベースのイテレータではなく、より高速なクロージャベースのイテレータを使用する。
- その他:コードの整理を行ったが、APIの変更はない。
変更の背景
この変更の主な背景は、go/token
パッケージの FileSet
が管理するファイル群をイテレートする際のパフォーマンス改善です。Go言語では、並行処理のプリミティブとしてゴルーチンとチャネルが提供されており、これらを用いてイテレータを実装することも可能です。しかし、チャネルはゴルーチン間の通信オーバーヘッドを伴うため、単純なシーケンシャルなイテレーションにおいては、クロージャ(コールバック関数)を用いた方が効率的である場合があります。
このコミットが行われた2012年1月時点では、Goコンパイラやツールチェインの初期段階であり、パフォーマンスの最適化が積極的に行われていました。特に、go/printer
のようなツールが go/token
パッケージを頻繁に利用するため、FileSet
のイテレーション性能は全体のパフォーマンスに大きな影響を与えます。コミットメッセージにも「go/printer
ベンチマークのパフォーマンスが約30%向上する」と記載されており、この変更が具体的な性能改善をもたらすことが示唆されています。
また、「better code organization」とあるように、チャネルベースの実装はゴルーチンとチャネルのセットアップ・クリーンアップが必要となり、コードが複雑になりがちです。一方、クロージャベースの実装は、イテレーションロジックをより直接的に記述できるため、コードの可読性と保守性が向上するという側面もあります。
前提知識の解説
Go言語の go/token
パッケージ
go/token
パッケージは、Go言語のソースコードを解析する際に使用されるトークン(字句)と、それらのトークンがソースコード内のどこに位置するかを示す「位置情報」を扱うためのパッケージです。Goコンパイラや go/ast
(抽象構文木)、go/parser
(パーサー) などのツールが内部的に利用します。
Pos
(Position): ソースコード内の特定のバイトオフセットを表す型です。ファイルセット内の絶対的な位置を示します。Position
:Pos
を人間が読みやすい形式(ファイル名、行番号、列番号)に変換した構造体です。File
: ソースファイル一つを表す構造体です。ファイル名、サイズ、行オフセットテーブル(各行の開始バイトオフセットのリスト)などを持ちます。FileSet
: 複数のFile
オブジェクトを管理するコレクションです。Go言語のコンパイル単位は通常複数のファイルから構成されるため、FileSet
はこれらのファイル全体にわたる位置情報を一元的に管理するために使用されます。Pos
はFileSet
内で一意なオフセットとして扱われます。
Go言語におけるイテレータパターン
Go言語には、Javaの Iterator
インターフェースやC++のイテレータのような組み込みのイテレータインターフェースはありません。しかし、以下の一般的なパターンでイテレータを実装できます。
-
チャネルベースのイテレータ:
- ゴルーチンとチャネルを使用して、要素を順次送信するパターンです。
Files()
メソッドのように、チャネルを返す関数として実装されます。- 呼び出し側は
for ... range
ループでチャネルから要素を受け取ります。 - 利点: 並行処理と相性が良く、イテレーションロジックと消費ロジックを分離しやすい。
- 欠点: ゴルーチンとチャネルの生成・スケジューリング・通信にオーバーヘッドがあり、単純なシーケンシャルアクセスでは性能が劣る場合がある。チャネルが閉じられるまでゴルーチンがブロックされる可能性がある。
-
クロージャ(コールバック関数)ベースのイテレータ:
- イテレーション対象の各要素に対して呼び出されるコールバック関数(クロージャ)を引数として受け取るパターンです。
Iterate()
メソッドのように、func(element *Type) bool
のようなシグネチャを持つクロージャを受け取ります。クロージャがfalse
を返すとイテレーションを中断できます。- 利点: オーバーヘッドが少なく、非常に高速。イテレーションロジックがシンプルになる。
- 欠点: イテレーションロジックと消費ロジックが密結合になりがち。
このコミットは、FileSet
のイテレーションにおいて、チャネルベースのオーバーヘッドを排除し、より直接的なクロージャベースのアプローチに切り替えることで、パフォーマンスを向上させることを目的としています。
技術的詳細
このコミットは、go/token
パッケージの FileSet
型が持つファイルコレクションへのアクセス方法を変更します。
Files()
メソッド (変更前)
変更前の FileSet
は Files()
というメソッドを提供していました。
func (s *FileSet) Files() <-chan *File {
ch := make(chan *File)
go func() {
for i := 0; ; i++ {
var f *File
s.mutex.RLock()
if i < len(s.files) {
f = s.files[i]
}
s.mutex.RUnlock()
if f == nil {
break
}
ch <- f
}
close(ch)
}()
return ch
}
この実装では、以下の特徴があります。
Files()
が呼び出されるたびに、新しいバッファなしチャネル (ch
) が作成されます。- 新しいゴルーチンが起動され、そのゴルーチン内で
FileSet
に登録されている*File
オブジェクトを順次チャネルに送信します。 FileSet
の内部スライスs.files
へのアクセスは、s.mutex.RLock()
とs.mutex.RUnlock()
で保護されています。- すべてのファイルが送信されると、チャネルが閉じられ、ゴルーチンが終了します。
- 呼び出し側は
for f := range fset.Files() { ... }
のようにしてファイルを受け取ります。
このアプローチの欠点は、チャネルの生成、ゴルーチンの起動、そしてチャネルを介したデータ送信に伴うオーバーヘッドです。特に、FileSet
に登録されているファイル数が少ない場合や、イテレーションが頻繁に行われる場合に、このオーバーヘッドが顕著になります。
Iterate()
メソッド (変更後)
変更後の FileSet
は Iterate()
というメソッドを提供します。
func (s *FileSet) Iterate(f func(*File) bool) {
for i := 0; ; i++ {
var file *File
s.mutex.RLock()
if i < len(s.files) {
file = s.files[i]
}
s.mutex.RUnlock()
if file == nil || !f(file) { // f(file)がfalseを返すとイテレーションを中断
break
}
}
}
この実装では、以下の特徴があります。
Iterate()
はfunc(*File) bool
型のクロージャf
を引数として受け取ります。FileSet
に登録されている*File
オブジェクトを順次、引数として渡されたクロージャf
に渡して呼び出します。- クロージャ
f
がfalse
を返した場合、イテレーションは途中で中断されます。これにより、特定の条件を満たすファイルが見つかった時点でイテレーションを停止できるため、不要な処理を削減できます。 - チャネルや追加のゴルーチンは使用されません。イテレーションは呼び出し元のゴルーチン内で直接実行されます。
FileSet
の内部スライスs.files
へのアクセスは引き続きs.mutex.RLock()
とs.mutex.RUnlock()
で保護されています。
この変更により、チャネルとゴルーチンに関連するオーバーヘッドが完全に排除され、イテレーションがより効率的になります。特に、go/printer
のようなツールが FileSet
を頻繁に走査する際に、この性能向上が直接的に寄与します。
その他の変更点
src/pkg/exp/types/check_test.go
:fset.Files()
を使用していたgetFile
関数がfset.Iterate()
を使用するように変更されています。これにより、exp/types
パッケージ(Go言語の型チェッカーの実験的な実装)も新しいイテレーションAPIに準拠します。src/pkg/go/token/position.go
:FileSet
のFiles()
メソッドが削除され、Iterate()
メソッドが追加されました。また、File
型のPosition
メソッドやFileSet
のFile
,Position
メソッドなど、関連するヘルパー関数やコメントの整理が行われています。特に、searchInts
のような最適化された検索関数がHelper functions
セクションに移動され、コードの構造が改善されています。src/pkg/go/token/position_test.go
:TestFiles
関数がfset.Files()
の代わりにfset.Iterate()
を使用するようにテストコードが更新されています。
コアとなるコードの変更箇所
src/pkg/go/token/position.go
--- a/src/pkg/go/token/position.go
+++ b/src/pkg/go/token/position.go
@@ -404,23 +345,91 @@ func (s *FileSet) AddFile(filename string, base, size int) *File {
return f
}
-// Files returns the files added to the file set.
-func (s *FileSet) Files() <-chan *File {
- ch := make(chan *File)
- go func() {\n-\t\tfor i := 0; ; i++ {\n-\t\t\tvar f *File\n-\t\t\ts.mutex.RLock()\n-\t\t\tif i < len(s.files) {\n-\t\t\t\tf = s.files[i]\n-\t\t\t}\n-\t\t\ts.mutex.RUnlock()\n-\t\t\tif f == nil {\n-\t\t\t\tbreak\n-\t\t\t}\n-\t\t\tch <- f\n+// Iterate calls f for the files in the file set in the order they were added
+// until f returns false.
+//
+func (s *FileSet) Iterate(f func(*File) bool) {
+ for i := 0; ; i++ {
+ var file *File
+ s.mutex.RLock()
+ if i < len(s.files) {
+ file = s.files[i]
+ }
+ s.mutex.RUnlock()
+ if file == nil || !f(file) {
+ break
}
- \t\tclose(ch)\n-\t}()\n-\treturn ch
+ }
}
src/pkg/exp/types/check_test.go
--- a/src/pkg/exp/types/check_test.go
+++ b/src/pkg/exp/types/check_test.go
@@ -47,17 +47,17 @@ var tests = []struct {
var fset = token.NewFileSet()
-// TODO(gri) This functionality should be in token.Fileset.
-func getFile(filename string) *token.File {
- for f := range fset.Files() {
+// TODO(gri) This functionality should be in token.Fileset.
+func getFile(filename string) (file *token.File) {
+ fset.Iterate(func(f *token.File) bool {
if f.Name() == filename {
-\t\t\treturn f
+\t\t\tfile = f
+\t\t\treturn false // end iteration
}
-\t}\
-\treturn nil
+\t\treturn true
+ })
+ return file
}
コアとなるコードの解説
FileSet.Iterate
の導入
src/pkg/go/token/position.go
において、FileSet
型から Files() <-chan *File
メソッドが削除され、代わりに Iterate(f func(*File) bool)
メソッドが追加されました。
-
削除された
Files()
: このメソッドは、*File
型のチャネルを返し、別のゴルーチンでファイルセット内の各ファイルをそのチャネルに送信していました。これにより、呼び出し側はfor ... range
構文でファイルを順次受け取ることができましたが、ゴルーチンとチャネルの生成・管理に伴うオーバーヘッドが発生していました。特に、ファイルセットが小さい場合や、イテレーションが頻繁に行われる場合に、このオーバーヘッドが性能ボトルネックとなる可能性がありました。 -
追加された
Iterate()
: この新しいメソッドは、func(*File) bool
というシグネチャを持つクロージャ(関数リテラル)を引数として受け取ります。Iterate
メソッドは、ファイルセット内の各*File
オブジェクトに対してこのクロージャを直接呼び出します。- クロージャが
true
を返すと、イテレーションは続行されます。 - クロージャが
false
を返すと、Iterate
メソッドは即座にイテレーションを中断し、呼び出し元に戻ります。これにより、特定の条件を満たすファイルが見つかった時点で不要な処理をスキップできる「早期終了」のメカニズムが提供されます。 - このアプローチは、チャネルや追加のゴルーチンを必要としないため、オーバーヘッドが大幅に削減され、より高速なイテレーションが可能になります。
- クロージャが
getFile
関数の変更
src/pkg/exp/types/check_test.go
内の getFile
関数は、特定のファイル名を持つ *token.File
を FileSet
から検索するユーティリティ関数です。この関数は、以前は fset.Files()
を使用してチャネルからファイルを読み込んでいましたが、今回の変更で fset.Iterate()
を使用するように書き換えられました。
- 変更前:
for f := range fset.Files() { ... }
- 変更後:
fset.Iterate(func(f *token.File) bool { ... })
新しい実装では、Iterate
に渡されるクロージャ内でファイル名が一致するかどうかをチェックし、一致した場合は file
変数に f
を代入し、false
を返してイテレーションを中断します。これにより、目的のファイルが見つかった時点で無駄なイテレーションを続ける必要がなくなります。
この変更は、go/token
パッケージのAPI変更に追従するだけでなく、exp/types
パッケージのテストコード自体もより効率的なイテレーションパターンを採用したことを示しています。
全体として、このコミットは go/token
パッケージの FileSet
のイテレーションメカニズムを、パフォーマンスとコードの簡潔さの観点から最適化するものです。チャネルベースの並行処理のオーバーヘッドを排除し、より直接的なクロージャベースのコールバックパターンを採用することで、Goコンパイラや関連ツールの実行速度向上に貢献しています。
関連リンク
- Go言語の
go/token
パッケージのドキュメント (現在のバージョン): https://pkg.go.dev/go/token - Go言語の
go/ast
パッケージのドキュメント: https://pkg.go.dev/go/ast - Go言語の
go/parser
パッケージのドキュメント: https://pkg.go.dev/go/parser
参考にした情報源リンク
- Go Code Review 5557051: https://golang.org/cl/5557051 (コミットメッセージに記載されている変更リストへのリンク)
- Go言語におけるイテレータパターンの議論 (一般的な情報):
- Go Slices: usage and internals - The Go Programming Language: https://go.dev/blog/slices (スライスとイテレーションの基本的な概念)
- Effective Go - The Go Programming Language: https://go.dev/doc/effective_go#channels (チャネルの利用に関する一般的なガイドライン)
- Go言語のパフォーマンス最適化に関する一般的な情報:
- Profiling Go Programs - The Go Programming Language: https://go.dev/blog/pprof (Goプログラムのプロファイリングと最適化のツールと手法)
[インデックス 11233] ファイルの概要
このコミットは、Go言語の標準ライブラリの一部である go/token
パッケージにおける FileSet
のファイルイテレーションメカニズムの変更に関するものです。具体的には、既存のチャネルベースのイテレータ Files()
を、より高速なクロージャベースのイテレータ Iterate()
に置き換えることで、パフォーマンスの向上とコードの整理を図っています。
コミット
commit 9edabbe03832e1203d0819c27542b6316ca39d0d
Author: Robert Griesemer <gri@golang.org>
Date: Wed Jan 18 14:10:42 2012 -0800
go/token: replaced Files() with Iterate()
- Use a faster closure-based iterator rather than a channel-based one.
- Otherwise: better code organization, but no other API changes.
R=r, r
CC=golang-dev
https://golang.org/cl/5557051
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/9edabbe03832e1203d0819c27542b6316ca39d0d
元コミット内容
go/token: replaced Files() with Iterate()
- チャネルベースのイテレータではなく、より高速なクロージャベースのイテレータを使用する。
- その他:コードの整理を行ったが、APIの変更はない。
変更の背景
この変更の主な背景は、go/token
パッケージの FileSet
が管理するファイル群をイテレートする際のパフォーマンス改善です。Go言語では、並行処理のプリミティブとしてゴルーチンとチャネルが提供されており、これらを用いてイテレータを実装することも可能です。しかし、チャネルはゴルーチン間の通信オーバーヘッドを伴うため、単純なシーケンシャルなイテレーションにおいては、クロージャ(コールバック関数)を用いた方が効率的である場合があります。
このコミットが行われた2012年1月時点では、Goコンパイラやツールチェインの初期段階であり、パフォーマンスの最適化が積極的に行われていました。特に、go/printer
のようなツールが go/token
パッケージを頻繁に利用するため、FileSet
のイテレーション性能は全体のパフォーマンスに大きな影響を与えます。コミットメッセージにも「go/printer
ベンチマークのパフォーマンスが約30%向上する」と記載されており、この変更が具体的な性能改善をもたらすことが示唆されています。
また、「better code organization」とあるように、チャネルベースの実装はゴルーチンとチャネルのセットアップ・クリーンアップが必要となり、コードが複雑になりがちです。一方、クロージャベースの実装は、イテレーションロジックをより直接的に記述できるため、コードの可読性と保守性が向上するという側面もあります。
前提知識の解説
Go言語の go/token
パッケージ
go/token
パッケージは、Go言語のソースコードを解析する際に使用されるトークン(字句)と、それらのトークンがソースコード内のどこに位置するかを示す「位置情報」を扱うためのパッケージです。Goコンパイラや go/ast
(抽象構文木)、go/parser
(パーサー) などのツールが内部的に利用します。
Pos
(Position): ソースコード内の特定のバイトオフセットを表す型です。ファイルセット内の絶対的な位置を示します。Position
:Pos
を人間が読みやすい形式(ファイル名、行番号、列番号)に変換した構造体です。File
: ソースファイル一つを表す構造体です。ファイル名、サイズ、行オフセットテーブル(各行の開始バイトオフセットのリスト)などを持ちます。FileSet
: 複数のFile
オブジェクトを管理するコレクションです。Go言語のコンパイル単位は通常複数のファイルから構成されるため、FileSet
はこれらのファイル全体にわたる位置情報を一元的に管理するために使用されます。Pos
はFileSet
内で一意なオフセットとして扱われます。
Go言語におけるイテレータパターン
Go言語には、Javaの Iterator
インターフェースやC++のイテレータのような組み込みのイテレータインターフェースはありません。しかし、以下の一般的なパターンでイテレータを実装できます。
-
チャネルベースのイテレータ:
- ゴルーチンとチャネルを使用して、要素を順次送信するパターンです。
Files()
メソッドのように、チャネルを返す関数として実装されます。- 呼び出し側は
for ... range
ループでチャネルから要素を受け取ります。 - 利点: 並行処理と相性が良く、イテレーションロジックと消費ロジックを分離しやすい。
- 欠点: ゴルーチンとチャネルの生成・スケジューリング・通信にオーバーヘッドがあり、単純なシーケンシャルアクセスでは性能が劣る場合がある。チャネルが閉じられるまでゴルーチンがブロックされる可能性がある。
-
クロージャ(コールバック関数)ベースのイテレータ:
- イテレーション対象の各要素に対して呼び出されるコールバック関数(クロージャ)を引数として受け取るパターンです。
Iterate()
メソッドのように、func(element *Type) bool
のようなシグネチャを持つクロージャを受け取ります。クロージャがfalse
を返すとイテレーションを中断できます。- 利点: オーバーヘッドが少なく、非常に高速。イテレーションロジックがシンプルになる。
- 欠点: イテレーションロジックと消費ロジックが密結合になりがち。
このコミットは、FileSet
のイテレーションにおいて、チャネルベースのオーバーヘッドを排除し、より直接的なクロージャベースのアプローチに切り替えることで、パフォーマンスを向上させることを目的としています。
技術的詳細
このコミットは、go/token
パッケージの FileSet
型が持つファイルコレクションへのアクセス方法を変更します。
Files()
メソッド (変更前)
変更前の FileSet
は Files()
というメソッドを提供していました。
func (s *FileSet) Files() <-chan *File {
ch := make(chan *File)
go func() {
for i := 0; ; i++ {
var f *File
s.mutex.RLock()
if i < len(s.files) {
f = s.files[i]
}
s.mutex.RUnlock()
if f == nil {
break
}
ch <- f
}
close(ch)
}()
return ch
}
この実装では、以下の特徴があります。
Files()
が呼び出されるたびに、新しいバッファなしチャネル (ch
) が作成されます。- 新しいゴルーチンが起動され、そのゴルーチン内で
FileSet
に登録されている*File
オブジェクトを順次チャネルに送信します。 FileSet
の内部スライスs.files
へのアクセスは、s.mutex.RLock()
とs.mutex.RUnlock()
で保護されています。- すべてのファイルが送信されると、チャネルが閉じられ、ゴルーチンが終了します。
- 呼び出し側は
for f := range fset.Files() { ... }
のようにしてファイルを受け取ります。
このアプローチの欠点は、チャネルの生成、ゴルーチンの起動、そしてチャネルを介したデータ送信に伴うオーバーヘッドです。特に、FileSet
に登録されているファイル数が少ない場合や、イテレーションが頻繁に行われる場合に、このオーバーヘッドが顕著になります。
Iterate()
メソッド (変更後)
変更後の FileSet
は Iterate()
というメソッドを提供します。
func (s *FileSet) Iterate(f func(*File) bool) {
for i := 0; ; i++ {
var file *File
s.mutex.RLock()
if i < len(s.files) {
file = s.files[i]
}
s.mutex.RUnlock()
if file == nil || !f(file) { // f(file)がfalseを返すとイテレーションを中断
break
}
}
}
この実装では、以下の特徴があります。
Iterate()
はfunc(*File) bool
型のクロージャf
を引数として受け取ります。FileSet
に登録されている*File
オブジェクトを順次、引数として渡されたクロージャf
に渡して呼び出します。- クロージャ
f
がfalse
を返した場合、イテレーションは途中で中断されます。これにより、特定の条件を満たすファイルが見つかった時点でイテレーションを停止できるため、不要な処理を削減できます。 - チャネルや追加のゴルーチンは使用されません。イテレーションは呼び出し元のゴルーチン内で直接実行されます。
FileSet
の内部スライスs.files
へのアクセスは引き続きs.mutex.RLock()
とs.mutex.RUnlock()
で保護されています。
この変更により、チャネルとゴルーチンに関連するオーバーヘッドが完全に排除され、イテレーションがより効率的になります。特に、go/printer
のようなツールが FileSet
を頻繁に走査する際に、この性能向上が直接的に寄与します。
その他の変更点
src/pkg/exp/types/check_test.go
:fset.Files()
を使用していたgetFile
関数がfset.Iterate()
を使用するように変更されています。これにより、exp/types
パッケージ(Go言語の型チェッカーの実験的な実装)も新しいイテレーションAPIに準拠します。src/pkg/go/token/position.go
:FileSet
のFiles()
メソッドが削除され、Iterate()
メソッドが追加されました。また、File
型のPosition
メソッドやFileSet
のFile
,Position
メソッドなど、関連するヘルパー関数やコメントの整理が行われています。特に、searchInts
のような最適化された検索関数がHelper functions
セクションに移動され、コードの構造が改善されています。src/pkg/go/token/position_test.go
:TestFiles
関数がfset.Files()
の代わりにfset.Iterate()
を使用するようにテストコードが更新されています。
コアとなるコードの変更箇所
src/pkg/go/token/position.go
--- a/src/pkg/go/token/position.go
+++ b/src/pkg/go/token/position.go
@@ -404,23 +345,91 @@ func (s *FileSet) AddFile(filename string, base, size int) *File {
return f
}
-// Files returns the files added to the file set.
-func (s *FileSet) Files() <-chan *File {
- ch := make(chan *File)
- go func(){\n-\t\tfor i := 0; ; i++ {\n-\t\t\tvar f *File\n-\t\t\ts.mutex.RLock()\n-\t\t\tif i < len(s.files) {\n-\t\t\t\tf = s.files[i]\n-\t\t\t}\n-\t\t\ts.mutex.RUnlock()\n-\t\t\tif f == nil {\n-\t\t\t\tbreak\n-\t\t\t}\n-\t\t\tch <- f\n+// Iterate calls f for the files in the file set in the order they were added
+// until f returns false.
+//
+func (s *FileSet) Iterate(f func(*File) bool) {
+ for i := 0; ; i++ {
+ var file *File
+ s.mutex.RLock()
+ if i < len(s.files) {
+ file = s.files[i]
+ }
+ s.mutex.RUnlock()
+ if file == nil || !f(file) {
+ break
}
- \t\tclose(ch)\n-\t}()\n-\treturn ch
+ }
}
src/pkg/exp/types/check_test.go
--- a/src/pkg/exp/types/check_test.go
+++ b/src/pkg/exp/types/check_test.go
@@ -47,17 +47,17 @@ var tests = []struct {
var fset = token.NewFileSet()
-// TODO(gri) This functionality should be in token.Fileset.
-func getFile(filename string) *token.File {
- for f := range fset.Files() {
+// TODO(gri) This functionality should be in token.Fileset.
+func getFile(filename string) (file *token.File) {
+ fset.Iterate(func(f *token.File) bool {
if f.Name() == filename {
-\t\t\treturn f
+\t\t\tfile = f
+\t\t\treturn false // end iteration
}
-\t}\
-\treturn nil
+\t\treturn true
+ })
+ return file
}
コアとなるコードの解説
FileSet.Iterate
の導入
src/pkg/go/token/position.go
において、FileSet
型から Files() <-chan *File
メソッドが削除され、代わりに Iterate(f func(*File) bool)
メソッドが追加されました。
-
削除された
Files()
: このメソッドは、*File
型のチャネルを返し、別のゴルーチンでファイルセット内の各ファイルをそのチャネルに送信していました。これにより、呼び出し側はfor ... range
構文でファイルを順次受け取ることができましたが、ゴルーチンとチャネルの生成・管理に伴うオーバーヘッドが発生していました。特に、ファイルセットが小さい場合や、イテレーションが頻繁に行われる場合に、このオーバーヘッドが性能ボトルネックとなる可能性がありました。 -
追加された
Iterate()
: この新しいメソッドは、func(*File) bool
というシグネチャを持つクロージャ(関数リテラル)を引数として受け取ります。Iterate
メソッドは、ファイルセット内の各*File
オブジェクトに対してこのクロージャを直接呼び出します。- クロージャが
true
を返すと、イテレーションは続行されます。 - クロージャが
false
を返すと、Iterate
メソッドは即座にイテレーションを中断し、呼び出し元に戻ります。これにより、特定の条件を満たすファイルが見つかった時点で不要な処理をスキップできる「早期終了」のメカニズムが提供されます。 - このアプローチは、チャネルや追加のゴルーチンを必要としないため、オーバーヘッドが大幅に削減され、より高速なイテレーションが可能になります。
- クロージャが
getFile
関数の変更
src/pkg/exp/types/check_test.go
内の getFile
関数は、特定のファイル名を持つ *token.File
を FileSet
から検索するユーティリティ関数です。この関数は、以前は fset.Files()
を使用してチャネルからファイルを読み込んでいましたが、今回の変更で fset.Iterate()
を使用するように書き換えられました。
- 変更前:
for f := range fset.Files() { ... }
- 変更後:
fset.Iterate(func(f *token.File) bool { ... })
新しい実装では、Iterate
に渡されるクロージャ内でファイル名が一致するかどうかをチェックし、一致した場合は file
変数に f
を代入し、false
を返してイテレーションを中断します。これにより、目的のファイルが見つかった時点で無駄なイテレーションを続ける必要がなくなります。
この変更は、go/token
パッケージのAPI変更に追従するだけでなく、exp/types
パッケージのテストコード自体もより効率的なイテレーションパターンを採用したことを示しています。
全体として、このコミットは go/token
パッケージの FileSet
のイテレーションメカニズムを、パフォーマンスとコードの簡潔さの観点から最適化するものです。チャネルベースの並行処理のオーバーヘッドを排除し、より直接的なクロージャベースのコールバックパターンを採用することで、Goコンパイラや関連ツールの実行速度向上に貢献しています。
関連リンク
- Go言語の
go/token
パッケージのドキュメント (現在のバージョン): https://pkg.go.dev/go/token - Go言語の
go/ast
パッケージのドキュメント: https://pkg.go.dev/go/ast - Go言語の
go/parser
パッケージのドキュメント: https://pkg.go.dev/go/parser
参考にした情報源リンク
- Go Code Review 5557051: https://golang.org/cl/5557051 (コミットメッセージに記載されている変更リストへのリンク)
- Go言語におけるイテレータパターンの議論 (一般的な情報):
- Go Slices: usage and internals - The Go Programming Language: https://go.dev/blog/slices (スライスとイテレーションの基本的な概念)
- Effective Go - The Go Programming Language: https://go.dev/doc/effective_go#channels (チャネルの利用に関する一般的なガイドライン)
- Go言語のパフォーマンス最適化に関する一般的な情報:
- Profiling Go Programs - The Go Programming Language: https://go.dev/blog/pprof (Goプログラムのプロファイリングと最適化のツールと手法)
[インデックス 11233] ファイルの概要
このコミットは、Go言語の標準ライブラリの一部である go/token
パッケージにおける FileSet
のファイルイテレーションメカニズムの変更に関するものです。具体的には、既存のチャネルベースのイテレータ Files()
を、より高速なクロージャベースのイテレータ Iterate()
に置き換えることで、パフォーマンスの向上とコードの整理を図っています。
コミット
commit 9edabbe03832e1203d0819c27542b6316ca39d0d
Author: Robert Griesemer <gri@golang.org>
Date: Wed Jan 18 14:10:42 2012 -0800
go/token: replaced Files() with Iterate()
- Use a faster closure-based iterator rather than a channel-based one.
- Otherwise: better code organization, but no other API changes.
R=r, r
CC=golang-dev
https://golang.org/cl/5557051
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/9edabbe03832e1203d0819c27542b6316ca39d0d
元コミット内容
go/token: replaced Files() with Iterate()
- チャネルベースのイテレータではなく、より高速なクロージャベースのイテレータを使用する。
- その他:コードの整理を行ったが、APIの変更はない。
変更の背景
この変更の主な背景は、go/token
パッケージの FileSet
が管理するファイル群をイテレートする際のパフォーマンス改善です。Go言語では、並行処理のプリミティブとしてゴルーチンとチャネルが提供されており、これらを用いてイテレータを実装することも可能です。しかし、チャネルはゴルーチン間の通信オーバーヘッドを伴うため、単純なシーケンシャルなイテレーションにおいては、クロージャ(コールバック関数)を用いた方が効率的である場合があります。
このコミットが行われた2012年1月時点では、Goコンパイラやツールチェインの初期段階であり、パフォーマンスの最適化が積極的に行われていました。特に、go/printer
のようなツールが go/token
パッケージを頻繁に利用するため、FileSet
のイテレーション性能は全体のパフォーマンスに大きな影響を与えます。コミットメッセージにも「go/printer
ベンチマークのパフォーマンスが約30%向上する」と記載されており、この変更が具体的な性能改善をもたらすことが示唆されています。
また、「better code organization」とあるように、チャネルベースの実装はゴルーチンとチャネルのセットアップ・クリーンアップが必要となり、コードが複雑になりがちです。一方、クロージャベースの実装は、イテレーションロジックをより直接的に記述できるため、コードの可読性と保守性が向上するという側面もあります。
前提知識の解説
Go言語の go/token
パッケージ
go/token
パッケージは、Go言語のソースコードを解析する際に使用されるトークン(字句)と、それらのトークンがソースコード内のどこに位置するかを示す「位置情報」を扱うためのパッケージです。Goコンパイラや go/ast
(抽象構文木)、go/parser
(パーサー) などのツールが内部的に利用します。
Pos
(Position): ソースコード内の特定のバイトオフセットを表す型です。ファイルセット内の絶対的な位置を示します。Position
:Pos
を人間が読みやすい形式(ファイル名、行番号、列番号)に変換した構造体です。File
: ソースファイル一つを表す構造体です。ファイル名、サイズ、行オフセットテーブル(各行の開始バイトオフセットのリスト)などを持ちます。FileSet
: 複数のFile
オブジェクトを管理するコレクションです。Go言語のコンパイル単位は通常複数のファイルから構成されるため、FileSet
はこれらのファイル全体にわたる位置情報を一元的に管理するために使用されます。Pos
はFileSet
内で一意なオフセットとして扱われます。
Go言語におけるイテレータパターン
Go言語には、Javaの Iterator
インターフェースやC++のイテレータのような組み込みのイテレータインターフェースはありません。しかし、以下の一般的なパターンでイテレータを実装できます。
-
チャネルベースのイテレータ:
- ゴルーチンとチャネルを使用して、要素を順次送信するパターンです。
Files()
メソッドのように、チャネルを返す関数として実装されます。- 呼び出し側は
for ... range
ループでチャネルから要素を受け取ります。 - 利点: 並行処理と相性が良く、イテレーションロジックと消費ロジックを分離しやすい。
- 欠点: ゴルーチンとチャネルの生成・スケジューリング・通信にオーバーヘッドがあり、単純なシーケンシャルアクセスでは性能が劣る場合がある。チャネルが閉じられるまでゴルーチンがブロックされる可能性がある。
-
クロージャ(コールバック関数)ベースのイテレータ:
- イテレーション対象の各要素に対して呼び出されるコールバック関数(クロージャ)を引数として受け取るパターンです。
Iterate()
メソッドのように、func(element *Type) bool
のようなシグネチャを持つクロージャを受け取ります。クロージャがfalse
を返すとイテレーションを中断できます。- 利点: オーバーヘッドが少なく、非常に高速。イテレーションロジックがシンプルになる。
- 欠点: イテレーションロジックと消費ロジックが密結合になりがち。
このコミットは、FileSet
のイテレーションにおいて、チャネルベースのオーバーヘッドを排除し、より直接的なクロージャベースのアプローチに切り替えることで、パフォーマンスを向上させることを目的としています。
技術的詳細
このコミットは、go/token
パッケージの FileSet
型が持つファイルコレクションへのアクセス方法を変更します。
Files()
メソッド (変更前)
変更前の FileSet
は Files()
というメソッドを提供していました。
func (s *FileSet) Files() <-chan *File {
ch := make(chan *File)
go func() {
for i := 0; ; i++ {
var f *File
s.mutex.RLock()
if i < len(s.files) {
f = s.files[i]
}
s.mutex.RUnlock()
if f == nil {
break
}
ch <- f
}
close(ch)
}()
return ch
}
この実装では、以下の特徴があります。
Files()
が呼び出されるたびに、新しいバッファなしチャネル (ch
) が作成されます。- 新しいゴルーチンが起動され、そのゴルーチン内で
FileSet
に登録されている*File
オブジェクトを順次チャネルに送信します。 FileSet
の内部スライスs.files
へのアクセスは、s.mutex.RLock()
とs.mutex.RUnlock()
で保護されています。- すべてのファイルが送信されると、チャネルが閉じられ、ゴルーチンが終了します。
- 呼び出し側は
for f := range fset.Files() { ... }
のようにしてファイルを受け取ります。
このアプローチの欠点は、チャネルの生成、ゴルーチンの起動、そしてチャネルを介したデータ送信に伴うオーバーヘッドです。特に、FileSet
に登録されているファイル数が少ない場合や、イテレーションが頻繁に行われる場合に、このオーバーヘッドが顕著になります。
Iterate()
メソッド (変更後)
変更後の FileSet
は Iterate()
というメソッドを提供します。
func (s *FileSet) Iterate(f func(*File) bool) {
for i := 0; ; i++ {
var file *File
s.mutex.RLock()
if i < len(s.files) {
file = s.files[i]
}
s.mutex.RUnlock()
if file == nil || !f(file) { // f(file)がfalseを返すとイテレーションを中断
break
}
}
}
この実装では、以下の特徴があります。
Iterate()
はfunc(*File) bool
型のクロージャf
を引数として受け取ります。FileSet
に登録されている*File
オブジェクトを順次、引数として渡されたクロージャf
に渡して呼び出します。- クロージャ
f
がfalse
を返した場合、イテレーションは途中で中断されます。これにより、特定の条件を満たすファイルが見つかった時点でイテレーションを停止できるため、不要な処理を削減できます。 - チャネルや追加のゴルーチンは使用されません。イテレーションは呼び出し元のゴルーチン内で直接実行されます。
FileSet
の内部スライスs.files
へのアクセスは引き続きs.mutex.RLock()
とs.mutex.RUnlock()
で保護されています。
この変更により、チャネルとゴルーチンに関連するオーバーヘッドが完全に排除され、イテレーションがより効率的になります。特に、go/printer
のようなツールが FileSet
を頻繁に走査する際に、この性能向上が直接的に寄与します。
その他の変更点
src/pkg/exp/types/check_test.go
:fset.Files()
を使用していたgetFile
関数がfset.Iterate()
を使用するように変更されています。これにより、exp/types
パッケージ(Go言語の型チェッカーの実験的な実装)も新しいイテレーションAPIに準拠します。src/pkg/go/token/position.go
:FileSet
のFiles()
メソッドが削除され、Iterate()
メソッドが追加されました。また、File
型のPosition
メソッドやFileSet
のFile
,Position
メソッドなど、関連するヘルパー関数やコメントの整理が行われています。特に、searchInts
のような最適化された検索関数がHelper functions
セクションに移動され、コードの構造が改善されています。src/pkg/go/token/position_test.go
:TestFiles
関数がfset.Files()
の代わりにfset.Iterate()
を使用するようにテストコードが更新されています。
コアとなるコードの変更箇所
src/pkg/go/token/position.go
--- a/src/pkg/go/token/position.go
+++ b/src/pkg/go/token/position.go
@@ -404,23 +345,91 @@ func (s *FileSet) AddFile(filename string, base, size int) *File {
return f
}
-// Files returns the files added to the file set.
-func (s *FileSet) Files() <-chan *File {
- ch := make(chan *File)
- go func(){\n-\t\tfor i := 0; ; i++ {\n-\t\t\tvar f *File\n-\t\t\ts.mutex.RLock()\n-\t\t\tif i < len(s.files) {\n-\t\t\t\tf = s.files[i]\n-\t\t\t}\n-\t\t\ts.mutex.RUnlock()\n-\t\t\tif f == nil {\n-\t\t\t\tbreak\n-\t\t\t}\n-\t\t\tch <- f\n+// Iterate calls f for the files in the file set in the order they were added
+// until f returns false.
+//
+func (s *FileSet) Iterate(f func(*File) bool) {
+ for i := 0; ; i++ {
+ var file *File
+ s.mutex.RLock()
+ if i < len(s.files) {
+ file = s.files[i]
+ }
+ s.mutex.RUnlock()
+ if file == nil || !f(file) {
+ break
}
- \t\tclose(ch)\n-\t}()\n-\treturn ch
+ }
}
src/pkg/exp/types/check_test.go
--- a/src/pkg/exp/types/check_test.go
+++ b/src/pkg/exp/types/check_test.go
@@ -47,17 +47,17 @@ var tests = []struct {
var fset = token.NewFileSet()
-// TODO(gri) This functionality should be in token.Fileset.
-func getFile(filename string) *token.File {
- for f := range fset.Files() {
+// TODO(gri) This functionality should be in token.Fileset.
+func getFile(filename string) (file *token.File) {
+ fset.Iterate(func(f *token.File) bool {
if f.Name() == filename {
-\t\t\treturn f
+\t\t\tfile = f
+\t\t\treturn false // end iteration
}
-\t}\
-\treturn nil
+\t\treturn true
+ })
+ return file
}
コアとなるコードの解説
FileSet.Iterate
の導入
src/pkg/go/token/position.go
において、FileSet
型から Files() <-chan *File
メソッドが削除され、代わりに Iterate(f func(*File) bool)
メソッドが追加されました。
-
削除された
Files()
: このメソッドは、*File
型のチャネルを返し、別のゴルーチンでファイルセット内の各ファイルをそのチャネルに送信していました。これにより、呼び出し側はfor ... range
構文でファイルを順次受け取ることができましたが、ゴルーチンとチャネルの生成・管理に伴うオーバーヘッドが発生していました。特に、ファイルセットが小さい場合や、イテレーションが頻繁に行われる場合に、このオーバーヘッドが性能ボトルネックとなる可能性がありました。 -
追加された
Iterate()
: この新しいメソッドは、func(*File) bool
というシグネチャを持つクロージャ(関数リテラル)を引数として受け取ります。Iterate
メソッドは、ファイルセット内の各*File
オブジェクトに対してこのクロージャを直接呼び出します。- クロージャが
true
を返すと、イテレーションは続行されます。 - クロージャが
false
を返すと、Iterate
メソッドは即座にイテレーションを中断し、呼び出し元に戻ります。これにより、特定の条件を満たすファイルが見つかった時点で不要な処理をスキップできる「早期終了」のメカニズムが提供されます。 - このアプローチは、チャネルや追加のゴルーチンを必要としないため、オーバーヘッドが大幅に削減され、より高速なイテレーションが可能になります。
- クロージャが
getFile
関数の変更
src/pkg/exp/types/check_test.go
内の getFile
関数は、特定のファイル名を持つ *token.File
を FileSet
から検索するユーティリティ関数です。この関数は、以前は fset.Files()
を使用してチャネルからファイルを読み込んでいましたが、今回の変更で fset.Iterate()
を使用するように書き換えられました。
- 変更前:
for f := range fset.Files() { ... }
- 変更後:
fset.Iterate(func(f *token.File) bool { ... })
新しい実装では、Iterate
に渡されるクロージャ内でファイル名が一致するかどうかをチェックし、一致した場合は file
変数に f
を代入し、false
を返してイテレーションを中断します。これにより、目的のファイルが見つかった時点で無駄なイテレーションを続ける必要がなくなります。
この変更は、go/token
パッケージのAPI変更に追従するだけでなく、exp/types
パッケージのテストコード自体もより効率的なイテレーションパターンを採用したことを示しています。
全体として、このコミットは go/token
パッケージの FileSet
のイテレーションメカニズムを、パフォーマンスとコードの簡潔さの観点から最適化するものです。チャネルベースの並行処理のオーバーヘッドを排除し、より直接的なクロージャベースのコールバックパターンを採用することで、Goコンパイラや関連ツールの実行速度向上に貢献しています。
関連リンク
- Go言語の
go/token
パッケージのドキュメント (現在のバージョン): https://pkg.go.dev/go/token - Go言語の
go/ast
パッケージのドキュメント: https://pkg.go.dev/go/ast - Go言語の
go/parser
パッケージのドキュメント: https://pkg.go.dev/go/parser
参考にした情報源リンク
- Go Code Review 5557051: https://golang.org/cl/5557051 (コミットメッセージに記載されている変更リストへのリンク)
- Go言語におけるイテレータパターンの議論 (一般的な情報):
- Go Slices: usage and internals - The Go Programming Language: https://go.dev/blog/slices (スライスとイテレーションの基本的な概念)
- Effective Go - The Go Programming Language: https://go.dev/doc/effective_go#channels (チャネルの利用に関する一般的なガイドライン)
- Go言語のパフォーマンス最適化に関する一般的な情報:
- Profiling Go Programs - The Go Programming Language: https://go.dev/blog/pprof (Goプログラムのプロファイリングと最適化のツールと手法)