Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

[インデックス 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 はこれらのファイル全体にわたる位置情報を一元的に管理するために使用されます。PosFileSet 内で一意なオフセットとして扱われます。

Go言語におけるイテレータパターン

Go言語には、Javaの Iterator インターフェースやC++のイテレータのような組み込みのイテレータインターフェースはありません。しかし、以下の一般的なパターンでイテレータを実装できます。

  1. チャネルベースのイテレータ:

    • ゴルーチンとチャネルを使用して、要素を順次送信するパターンです。
    • Files() メソッドのように、チャネルを返す関数として実装されます。
    • 呼び出し側は for ... range ループでチャネルから要素を受け取ります。
    • 利点: 並行処理と相性が良く、イテレーションロジックと消費ロジックを分離しやすい。
    • 欠点: ゴルーチンとチャネルの生成・スケジューリング・通信にオーバーヘッドがあり、単純なシーケンシャルアクセスでは性能が劣る場合がある。チャネルが閉じられるまでゴルーチンがブロックされる可能性がある。
  2. クロージャ(コールバック関数)ベースのイテレータ:

    • イテレーション対象の各要素に対して呼び出されるコールバック関数(クロージャ)を引数として受け取るパターンです。
    • Iterate() メソッドのように、func(element *Type) bool のようなシグネチャを持つクロージャを受け取ります。クロージャが false を返すとイテレーションを中断できます。
    • 利点: オーバーヘッドが少なく、非常に高速。イテレーションロジックがシンプルになる。
    • 欠点: イテレーションロジックと消費ロジックが密結合になりがち。

このコミットは、FileSet のイテレーションにおいて、チャネルベースのオーバーヘッドを排除し、より直接的なクロージャベースのアプローチに切り替えることで、パフォーマンスを向上させることを目的としています。

技術的詳細

このコミットは、go/token パッケージの FileSet 型が持つファイルコレクションへのアクセス方法を変更します。

Files() メソッド (変更前)

変更前の FileSetFiles() というメソッドを提供していました。

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() メソッド (変更後)

変更後の FileSetIterate() というメソッドを提供します。

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 に渡して呼び出します。
  • クロージャ ffalse を返した場合、イテレーションは途中で中断されます。これにより、特定の条件を満たすファイルが見つかった時点でイテレーションを停止できるため、不要な処理を削減できます。
  • チャネルや追加のゴルーチンは使用されません。イテレーションは呼び出し元のゴルーチン内で直接実行されます。
  • 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: FileSetFiles() メソッドが削除され、Iterate() メソッドが追加されました。また、File 型の Position メソッドや FileSetFile, 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.FileFileSet から検索するユーティリティ関数です。この関数は、以前は 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 Code Review 5557051: https://golang.org/cl/5557051 (コミットメッセージに記載されている変更リストへのリンク)
  • Go言語におけるイテレータパターンの議論 (一般的な情報):
  • 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 はこれらのファイル全体にわたる位置情報を一元的に管理するために使用されます。PosFileSet 内で一意なオフセットとして扱われます。

Go言語におけるイテレータパターン

Go言語には、Javaの Iterator インターフェースやC++のイテレータのような組み込みのイテレータインターフェースはありません。しかし、以下の一般的なパターンでイテレータを実装できます。

  1. チャネルベースのイテレータ:

    • ゴルーチンとチャネルを使用して、要素を順次送信するパターンです。
    • Files() メソッドのように、チャネルを返す関数として実装されます。
    • 呼び出し側は for ... range ループでチャネルから要素を受け取ります。
    • 利点: 並行処理と相性が良く、イテレーションロジックと消費ロジックを分離しやすい。
    • 欠点: ゴルーチンとチャネルの生成・スケジューリング・通信にオーバーヘッドがあり、単純なシーケンシャルアクセスでは性能が劣る場合がある。チャネルが閉じられるまでゴルーチンがブロックされる可能性がある。
  2. クロージャ(コールバック関数)ベースのイテレータ:

    • イテレーション対象の各要素に対して呼び出されるコールバック関数(クロージャ)を引数として受け取るパターンです。
    • Iterate() メソッドのように、func(element *Type) bool のようなシグネチャを持つクロージャを受け取ります。クロージャが false を返すとイテレーションを中断できます。
    • 利点: オーバーヘッドが少なく、非常に高速。イテレーションロジックがシンプルになる。
    • 欠点: イテレーションロジックと消費ロジックが密結合になりがち。

このコミットは、FileSet のイテレーションにおいて、チャネルベースのオーバーヘッドを排除し、より直接的なクロージャベースのアプローチに切り替えることで、パフォーマンスを向上させることを目的としています。

技術的詳細

このコミットは、go/token パッケージの FileSet 型が持つファイルコレクションへのアクセス方法を変更します。

Files() メソッド (変更前)

変更前の FileSetFiles() というメソッドを提供していました。

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() メソッド (変更後)

変更後の FileSetIterate() というメソッドを提供します。

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 に渡して呼び出します。
  • クロージャ ffalse を返した場合、イテレーションは途中で中断されます。これにより、特定の条件を満たすファイルが見つかった時点でイテレーションを停止できるため、不要な処理を削減できます。
  • チャネルや追加のゴルーチンは使用されません。イテレーションは呼び出し元のゴルーチン内で直接実行されます。
  • 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: FileSetFiles() メソッドが削除され、Iterate() メソッドが追加されました。また、File 型の Position メソッドや FileSetFile, 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.FileFileSet から検索するユーティリティ関数です。この関数は、以前は 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 Code Review 5557051: https://golang.org/cl/5557051 (コミットメッセージに記載されている変更リストへのリンク)
  • Go言語におけるイテレータパターンの議論 (一般的な情報):
  • 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 はこれらのファイル全体にわたる位置情報を一元的に管理するために使用されます。PosFileSet 内で一意なオフセットとして扱われます。

Go言語におけるイテレータパターン

Go言語には、Javaの Iterator インターフェースやC++のイテレータのような組み込みのイテレータインターフェースはありません。しかし、以下の一般的なパターンでイテレータを実装できます。

  1. チャネルベースのイテレータ:

    • ゴルーチンとチャネルを使用して、要素を順次送信するパターンです。
    • Files() メソッドのように、チャネルを返す関数として実装されます。
    • 呼び出し側は for ... range ループでチャネルから要素を受け取ります。
    • 利点: 並行処理と相性が良く、イテレーションロジックと消費ロジックを分離しやすい。
    • 欠点: ゴルーチンとチャネルの生成・スケジューリング・通信にオーバーヘッドがあり、単純なシーケンシャルアクセスでは性能が劣る場合がある。チャネルが閉じられるまでゴルーチンがブロックされる可能性がある。
  2. クロージャ(コールバック関数)ベースのイテレータ:

    • イテレーション対象の各要素に対して呼び出されるコールバック関数(クロージャ)を引数として受け取るパターンです。
    • Iterate() メソッドのように、func(element *Type) bool のようなシグネチャを持つクロージャを受け取ります。クロージャが false を返すとイテレーションを中断できます。
    • 利点: オーバーヘッドが少なく、非常に高速。イテレーションロジックがシンプルになる。
    • 欠点: イテレーションロジックと消費ロジックが密結合になりがち。

このコミットは、FileSet のイテレーションにおいて、チャネルベースのオーバーヘッドを排除し、より直接的なクロージャベースのアプローチに切り替えることで、パフォーマンスを向上させることを目的としています。

技術的詳細

このコミットは、go/token パッケージの FileSet 型が持つファイルコレクションへのアクセス方法を変更します。

Files() メソッド (変更前)

変更前の FileSetFiles() というメソッドを提供していました。

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() メソッド (変更後)

変更後の FileSetIterate() というメソッドを提供します。

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 に渡して呼び出します。
  • クロージャ ffalse を返した場合、イテレーションは途中で中断されます。これにより、特定の条件を満たすファイルが見つかった時点でイテレーションを停止できるため、不要な処理を削減できます。
  • チャネルや追加のゴルーチンは使用されません。イテレーションは呼び出し元のゴルーチン内で直接実行されます。
  • 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: FileSetFiles() メソッドが削除され、Iterate() メソッドが追加されました。また、File 型の Position メソッドや FileSetFile, 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.FileFileSet から検索するユーティリティ関数です。この関数は、以前は 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 Code Review 5557051: https://golang.org/cl/5557051 (コミットメッセージに記載されている変更リストへのリンク)
  • Go言語におけるイテレータパターンの議論 (一般的な情報):
  • Go言語のパフォーマンス最適化に関する一般的な情報:
    • Profiling Go Programs - The Go Programming Language: https://go.dev/blog/pprof (Goプログラムのプロファイリングと最適化のツールと手法)