[インデックス 18747] ファイルの概要
このコミットは、Go言語の標準ライブラリ os パッケージにおけるWindows環境でのファイルオープン処理の改善に関するものです。具体的には、os.OpenFile 関数がファイルとディレクトリのどちらを開こうとするかの試行順序を変更し、パフォーマンスの最適化とエラーハンドリングの改善を図っています。
コミット
commit 9a7cd11bc8f1763710a18bd90e9db00f8281d69b
Author: Patrick Mézard <patrick@mezard.eu>
Date: Wed Mar 5 12:19:56 2014 +1100
os: try openFile before openDir in windows os.OpenFile
Logging calls when running "go install -a std" turns:
547 openDir succeeded
3593 openDir failed and fell back to openFile
3592 openFile succeeded
1 both failed
into:
3592 openFile succeeded
548 openFile failed and fell back
547 openDir succeeded
1 both failed
Here the change trades 3593 failed openDir for 548 failed openFile.
Fix issue 7426.
LGTM=alex.brainman
R=golang-codereviews, alex.brainman, bradfitz
CC=golang-codereviews
https://golang.org/cl/70480044
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/9a7cd11bc8f1763710a18bd90e9db00f8281d69b
元コミット内容
os: try openFile before openDir in windows os.OpenFile
このコミットは、Windows環境における os.OpenFile 関数内で、ファイルを開く試行 (openFile) をディレクトリを開く試行 (openDir) よりも先に行うように変更します。
コミットメッセージには、go install -a std を実行した際のログの変化が示されています。変更前は、ディレクトリとして開こうとして失敗し、その後ファイルとして開くというパターンが3593回発生していましたが、変更後はファイルとして開こうとして失敗し、その後ディレクトリとして開くというパターンが548回に減少しています。これにより、全体として失敗する試行の回数が減り、パフォーマンスが向上することが示唆されています。
この変更は、Issue 7426 を修正するものです。
変更の背景
Go言語の os.OpenFile 関数は、指定されたパスが通常のファイルであるかディレクトリであるかを区別して開く必要があります。Windowsのファイルシステムでは、ファイルとディレクトリは異なる方法で扱われます。
従来の os.OpenFile の実装では、まずパスをディレクトリとして開こうと試み、それが失敗した場合にファイルとして開こうとフォールバックしていました。この順序は、特定のシナリオ、特に多くのファイルが存在する環境や、go install -a std のようなビルドプロセスにおいて、非効率的であることが判明しました。
コミットメッセージに示されているログデータは、この非効率性を明確に示しています。
-
変更前:
openDir succeeded: 547回 (ディレクトリとして正しく開けたケース)openDir failed and fell back to openFile: 3593回 (ディレクトリとして開こうとして失敗し、ファイルとして再試行したケース)openFile succeeded: 3592回 (ファイルとして正しく開けたケース)both failed: 1回 (両方失敗したケース) このデータから、多くのケースで実際にはファイルであるパスに対して、最初にディレクトリとして開こうとする無駄な試行が発生していたことがわかります。
-
変更後:
openFile succeeded: 3592回 (ファイルとして正しく開けたケース)openFile failed and fell back: 548回 (ファイルとして開こうとして失敗し、ディレクトリとして再試行したケース)openDir succeeded: 547回 (ディレクトリとして正しく開けたケース)both failed: 1回 (両方失敗したケース) この変更により、openDirの失敗回数が大幅に減少し、代わりにopenFileの失敗回数が増加していますが、その数はopenDirの失敗回数よりもはるかに少ないです。これは、ほとんどのOpenFileの呼び出しが実際にはファイルを対象としているため、ファイルとして先に試行する方が効率的であることを示しています。
この最適化は、Goのビルドプロセスなど、多数のファイル操作が行われる場面でのパフォーマンス改善に寄与します。
前提知識の解説
このコミットを理解するためには、以下の概念について知っておく必要があります。
-
os.OpenFile関数: Go言語のosパッケージが提供する関数で、指定されたパスのファイルを開きます。ファイルが存在しない場合は作成したり、読み書きのモードを指定したり、パーミッションを設定したりできます。内部的には、オペレーティングシステム固有のシステムコールを呼び出してファイル操作を行います。 -
Windowsファイルシステムとファイル/ディレクトリの区別: Windowsオペレーティングシステムでは、ファイルとディレクトリは異なるオブジェクトとして扱われます。ファイルを開くためのAPIとディレクトリを開くためのAPIは異なります。例えば、Win32 APIでは
CreateFile関数がファイルとディレクトリの両方を開くために使用されますが、その際に渡すフラグによって挙動が変わります。Goのosパッケージは、これらの低レベルなシステムコールを抽象化し、クロスプラットフォームなインターフェースを提供しています。 -
openDirとopenFile(Go内部関数): GoのosパッケージのWindows実装 (src/pkg/os/file_windows.go) には、内部的にopenDirとopenFileという関数が存在します。openDir(name string): 指定されたパスをディレクトリとして開こうと試みます。成功すればディレクトリを表す*Fileオブジェクトを返します。openFile(name string, flag int, perm FileMode): 指定されたパスを通常のファイルとして開こうと試みます。成功すればファイルを表す*Fileオブジェクトを返します。
-
syscall.ENOENTとsyscall.EISDIR: これらはGoのsyscallパッケージで定義されているエラーコードです。syscall.ENOENT(Error NO ENTry): 指定されたファイルやディレクトリが存在しない場合に返されるエラーです。syscall.EISDIR(Error Is DIRectory): ファイルとして開こうとしたパスが実際にはディレクトリであった場合に返されるエラーです。os.OpenFileがファイルとして開こうとした際に、それがディレクトリであった場合にこのエラーを返すことで、呼び出し元に適切な情報を提供します。
-
go install -a std: Goのコマンドラインツールの一つで、標準ライブラリのすべてのパッケージを強制的に再ビルドしてインストールします。このプロセスでは、Goのツールチェインが標準ライブラリ内の多数のファイルやディレクトリに対してos.OpenFileのような操作を頻繁に実行します。そのため、このコマンドの実行時のパフォーマンスは、ファイル操作の効率に大きく依存します。
技術的詳細
このコミットの技術的な核心は、os.OpenFile 関数における openFile と openDir の呼び出し順序の変更です。
変更前のコードでは、os.OpenFile はまず openDir(name) を呼び出して、指定された name がディレクトリであるかどうかを試行していました。
// 変更前
r, e := openDir(name)
if e == nil {
// ディレクトリとして開けた場合
// ...
return r, nil
}
// ディレクトリとして開けなかった場合、ファイルとして試行
r, e = openFile(name, flag, perm)
if e == nil {
return r, nil
}
return nil, &PathError{"open", name, e}
このロジックの問題点は、ほとんどの os.OpenFile の呼び出しが実際には通常のファイルを対象としているにもかかわらず、最初にディレクトリとして開こうとする無駄なシステムコールが発生していたことです。ディレクトリとして開こうとして失敗した場合(例えば、name がファイルであった場合)、openDir はエラーを返し、その後 openFile が呼び出されていました。この「試行→失敗→フォールバック」のサイクルが、特に多数のファイル操作が行われるシナリオでオーバーヘッドとなっていました。
変更後のコードでは、この順序が逆転しています。まず openFile(name, flag, perm) を呼び出して、指定された name が通常のファイルであるかどうかを試行します。
// 変更後
r, errf := openFile(name, flag, perm)
if errf == nil {
return r, nil
}
// ファイルとして開けなかった場合、ディレクトリとして試行
r, errd := openDir(name)
if errd == nil {
// ディレクトリとして開けた場合
// ...
return r, nil
}
return nil, &PathError{"open", name, errf} // 最終的なエラーはopenFileのものを返す
この変更により、もし name が通常のファイルであれば、最初の openFile の呼び出しで成功し、openDir の呼び出しはスキップされます。これは、ほとんどの OpenFile の呼び出しがファイルを対象としているという前提に基づくと、より効率的なパスとなります。
もし name がディレクトリであった場合、openFile は syscall.EISDIR エラーを返すか、あるいは他のエラーを返す可能性があります。その場合、コードは openDir を試行し、ディレクトリとして正しく開くことができれば、その *File オブジェクトを返します。
重要なのは、最終的にエラーを返す際に、openFile のエラー (errf) を優先して返している点です。これは、もしファイルとして開こうとして失敗し、かつディレクトリとしても開けなかった場合、ユーザーにとっては「ファイルとして開けなかった」という情報がより適切であるという判断に基づいていると考えられます。
この変更は、Windows環境における os.OpenFile の内部実装に特化したものであり、他のオペレーティングシステムには影響しません。WindowsのファイルシステムAPIの特性と、一般的なファイルアクセスのパターンを考慮した最適化と言えます。
コアとなるコードの変更箇所
変更は src/pkg/os/file_windows.go ファイルの OpenFile 関数内で行われています。
--- a/src/pkg/os/file_windows.go
+++ b/src/pkg/os/file_windows.go
@@ -134,20 +134,19 @@ func OpenFile(name string, flag int, perm FileMode) (file *File, err error) {
if name == "" {
return nil, &PathError{"open", name, syscall.ENOENT}
}
- // TODO(brainman): not sure about my logic of assuming it is dir first, then fall back to file
- r, e := openDir(name)
- if e == nil {
+ r, errf := openFile(name, flag, perm)
+ if errf == nil {
+ return r, nil
+ }
+ r, errd := openDir(name)
+ if errd == nil {
if flag&O_WRONLY != 0 || flag&O_RDWR != 0 {
r.Close()
return nil, &PathError{"open", name, syscall.EISDIR}
}
return r, nil
}
- r, e = openFile(name, flag, perm)
- if e == nil {
- return r, nil
- }
- return nil, &PathError{"open", name, e}
+ return nil, &PathError{"open", name, errf}
}
// Close closes the File, rendering it unusable for I/O.
コアとなるコードの解説
変更前のコードでは、OpenFile 関数は以下のロジックで動作していました。
openDir(name)を呼び出し、nameがディレクトリとして開けるか試行します。- もし
openDirが成功 (e == nil) し、かつ書き込みモード (O_WRONLYまたはO_RDWR) でない場合、ディレクトリとして開いた*Fileオブジェクトを返します。書き込みモードの場合は、ディレクトリへの書き込みは許可されないためEISDIRエラーを返して閉じます。 - もし
openDirが失敗した場合(e != nil)、次にopenFile(name, flag, perm)を呼び出し、nameが通常のファイルとして開けるか試行します。 - もし
openFileが成功 (e == nil) した場合、ファイルとして開いた*Fileオブジェクトを返します。 - 両方の試行が失敗した場合、最後に
openFileが返したエラーeを含むPathErrorを返します。
変更後のコードでは、この順序が逆転し、エラーハンドリングも少し変更されています。
openFile(name, flag, perm)を呼び出し、nameが通常のファイルとして開けるか試行します。この結果はerrfに格納されます。- もし
openFileが成功 (errf == nil) した場合、ファイルとして開いた*Fileオブジェクトを即座に返します。この場合、openDirは呼び出されません。 - もし
openFileが失敗した場合(errf != nil)、次にopenDir(name)を呼び出し、nameがディレクトリとして開けるか試行します。この結果はerrdに格納されます。 - もし
openDirが成功 (errd == nil) し、かつ書き込みモードでない場合、ディレクトリとして開いた*Fileオブジェクトを返します。書き込みモードの場合は、ディレクトリへの書き込みは許可されないためEISDIRエラーを返して閉じます。 - 両方の試行が失敗した場合、最終的に
openFileが返したエラーerrfを含むPathErrorを返します。これは、元のコードがopenFileのエラーを最終的に返していた挙動を維持しています。
この変更により、Goの os.OpenFile がWindows上で動作する際に、ファイルを開くケースが圧倒的に多いという実情に合わせて、より効率的なパスが選択されるようになりました。これにより、不要なシステムコールが減少し、特に多数のファイル操作を伴うアプリケーション(Goのビルドツールなど)のパフォーマンスが向上します。
関連リンク
- Go Issue 7426: https://code.google.com/p/go/issues/detail?id=7426 (古いGoogle Codeのリンクですが、コミットメッセージに記載されています)
- Go Change List 70480044: https://golang.org/cl/70480044 (Goのコードレビューシステムへのリンク)
参考にした情報源リンク
- Go言語の公式ドキュメント (
osパッケージ): https://pkg.go.dev/os - Go言語のソースコード (
src/pkg/os/file_windows.go): https://github.com/golang/go/blob/master/src/os/file_windows.go (現在のパスはsrc/os/file_windows.goに変更されていますが、当時のパスはsrc/pkg/os/file_windows.goでした) - Windows API
CreateFile関数に関する情報 (Microsoft Learn): https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-createfilew syscallパッケージのエラーコードに関する情報: https://pkg.go.dev/syscall