[インデックス 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