[インデックス 18021] ファイルの概要
このコミットは、Go言語の標準ライブラリos
パッケージ内のreaddir
関数における文字列結合の最適化に関するものです。具体的には、ディレクトリ内のファイル情報を読み取る際に発生していた不要な文字列結合を削除することで、パフォーマンスの改善を図っています。
コミット
commit ff8e45828c044665b60c37287e4f2d9e91754333
Author: Brad Fitzpatrick <bradfitz@golang.org>
Date: Tue Dec 17 12:25:32 2013 -0800
os: avoid a string concat in readdir
R=golang-dev, crawshaw
CC=golang-dev
https://golang.org/cl/37690045
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/ff8e45828c044665b60c37287e4f2d9e91754333
元コミット内容
os: avoid a string concat in readdir
R=golang-dev, crawshaw
CC=golang-dev
https://golang.org/cl/37690045
変更の背景
このコミットの背景には、Go言語における文字列結合のパフォーマンス特性と、ファイルシステム操作における効率性の追求があります。Go言語では、文字列はイミュータブル(不変)な型です。そのため、+
演算子を使って文字列を結合するたびに、新しい文字列が生成され、メモリの再割り当てとコピーが発生します。ループ内で頻繁に文字列結合を行うと、このオーバーヘッドが蓄積され、パフォーマンスに大きな影響を与える可能性があります。
os
パッケージのreaddir
関数は、ディレクトリ内のエントリ(ファイルやサブディレクトリ)の名前を読み取り、それぞれのFileInfo
を取得するためにlstat
システムコールを呼び出します。lstat
には、ファイルへの完全なパスを渡す必要があります。以前の実装では、ディレクトリ名とファイル名を結合する際に、余分な文字列結合が行われていました。このコミットは、この冗長な結合を排除し、readdir
関数の効率を向上させることを目的としています。特に、多数のファイルを含むディレクトリを読み取る場合に、この最適化が顕著な効果を発揮すると考えられます。
前提知識の解説
Go言語における文字列とパフォーマンス
Go言語の文字列はバイトの読み取り専用スライスであり、一度作成されると内容を変更できません。この特性は、文字列の安全な共有を可能にする一方で、文字列結合の際には注意が必要です。
+
演算子による結合: 最も直感的ですが、複数の文字列を+
で結合すると、中間的な文字列が多数生成され、ガベージコレクションの負荷が増大し、パフォーマンスが低下する可能性があります。strings.Builder
: 複数の文字列を効率的に構築するための推奨される方法です。内部的に可変なバイトバッファを使用し、メモリの再割り当てを最小限に抑えます。strings.Join()
: 文字列のスライスを特定の区切り文字で結合する場合に非常に効率的です。fmt.Sprintf()
: フォーマットされた文字列を生成する際に使用しますが、純粋な文字列結合目的ではstrings.Builder
やstrings.Join()
よりも遅い傾向があります。
このコミットが行われた2013年時点では、strings.Builder
はまだ存在していませんでしたが、+
演算子による不要な結合を避けるという原則は当時から重要でした。
os
パッケージとファイルシステム操作
os
パッケージは、オペレーティングシステムとの基本的なインタラクションを提供します。ファイルやディレクトリの操作、プロセスの管理、環境変数の取得などが含まれます。
os.File.Readdirnames(n int)
:File
オブジェクトのメソッドで、ディレクトリ内のエントリ名を文字列スライスとして返します。n
が0より大きい場合、最大n
個のエントリを返します。n
が0以下の場合、すべてのエントリを返します。os.Lstat(path string)
: 指定されたパスのファイル情報を返します。シンボリックリンクの場合、リンク自体ではなく、リンク先のファイル情報を返します。この関数は、ファイルが存在するかどうか、種類(ファイル、ディレクトリ、シンボリックリンクなど)、サイズ、パーミッションなどを確認するために使用されます。os.IsNotExist(err error)
: エラーがファイルまたはディレクトリが存在しないことを示す場合にtrue
を返します。
パス区切り文字
Unix系システムではパス区切り文字として/
が使用されます。Windowsでは\
が使用されますが、Goのfilepath
パッケージはクロスプラットフォームなパス操作を提供し、内部的に適切な区切り文字を扱います。このコミットはUnix固有のファイルfile_unix.go
に対する変更であるため、/
が直接扱われています。
技術的詳細
このコミットは、src/pkg/os/file_unix.go
ファイル内のFile
構造体のreaddir
メソッドに対する変更です。このメソッドは、ディレクトリの内容を読み取り、それぞれのFileInfo
を返す役割を担っています。
変更前のコードでは、dirname
(ディレクトリのパス)が空の場合に"."
に設定され、その後、常にdirname += "/"
という文字列結合が行われていました。これにより、dirname
の末尾に/
が追加されていました。
// 変更前
if dirname == "" {
dirname = "."
}
dirname += "/" // ここで余分な文字列結合が発生
names, err := f.Readdirnames(n)
fi = make([]FileInfo, 0, len(names))
for _, filename := range names {
fip, lerr := lstat(dirname + filename) // ここでも結合
// ...
}
そして、ループ内で各filename
に対してlstat(dirname + filename)
という形でパスを構築していました。このdirname + filename
の結合は、dirname
が既に/
で終わっているため、結果的にdirname/filename
という形式になります。
このコミットでは、dirname += "/"
という行を削除し、代わりにlstat
呼び出しの中でdirname + "/" + filename
という形で明示的に/
を挿入するように変更しています。
// 変更後
if dirname == "" {
dirname = "."
}
// dirname += "/" は削除された
names, err := f.Readdirnames(n)
fi = make([]FileInfo, 0, len(names))
for _, filename := range names {
fip, lerr := lstat(dirname + "/" + filename) // ここで結合
// ...
}
一見すると、dirname + "/"
を削除してdirname + "/" + filename
にすることで、結合回数が変わらないように見えるかもしれません。しかし、元のコードではdirname += "/"
によってdirname
変数自体が新しい文字列オブジェクトに置き換えられていました。これはループの外で行われるため、一度だけ発生するオーバーヘッドです。
しかし、より重要なのは、lstat(dirname + filename)
という結合が、dirname
が既に/
で終わっていることを前提としていた点です。この変更により、dirname
が/
で終わっているかどうかに関わらず、常にdirname
とfilename
の間に/
を挿入する形になります。
この変更の真の意図は、dirname += "/"
という行が不要な文字列の再構築を引き起こしていた可能性を排除すること、そしてlstat
に渡すパスの構築をより直接的に行うことにあると考えられます。特に、dirname
が"."
の場合、"./"
というパスが生成され、その後に"./filename"
という結合が行われていました。この変更により、dirname
が"."
の場合でも、"." + "/" + filename
となり、結果的に"./filename"
というパスが生成されます。
この最適化は、特に多数のファイルを含むディレクトリを読み取る際に、lstat
呼び出しごとに発生する文字列結合の効率をわずかに向上させることを目的としています。Goのコンパイラは文字列結合を最適化する場合がありますが、このような明示的な変更は、コンパイラが最適化しにくいケースや、より明確な意図を示すために行われることがあります。
コアとなるコードの変更箇所
src/pkg/os/file_unix.go
ファイルのreaddir
関数内。
--- a/src/pkg/os/file_unix.go
+++ b/src/pkg/os/file_unix.go
@@ -160,11 +160,10 @@ func (f *File) readdir(n int) (fi []FileInfo, err error) {
if dirname == "" {
dirname = "."
}
- dirname += "/" // この行が削除された
names, err := f.Readdirnames(n)
fi = make([]FileInfo, 0, len(names))
for _, filename := range names {
- fip, lerr := lstat(dirname + filename) // 変更前
+ fip, lerr := lstat(dirname + "/" + filename) // 変更後
if IsNotExist(lerr) {
// File disappeared between readdir + stat.
// Just treat it as if it didn't exist.
コアとなるコードの解説
変更の核心は、dirname += "/"
という行の削除と、それに伴うlstat
呼び出し内のパス構築方法の変更です。
-
dirname += "/"
の削除:- 元のコードでは、
dirname
が空文字列の場合に"."
に設定された後、無条件にdirname
の末尾に/
が追加されていました。これにより、dirname
は常に/
で終わる文字列になっていました(例:"/path/to/dir/"
または"./"
)。 - この操作自体が文字列結合であり、新しい文字列オブジェクトを生成していました。このコミットでは、この余分な文字列結合を排除しています。
- 元のコードでは、
-
fip, lerr := lstat(dirname + "/" + filename)
への変更:dirname += "/"
が削除されたため、dirname
はもはや常に/
で終わるわけではありません(例:"/path/to/dir"
または"."
)。- そのため、
lstat
に渡すパスを正しく構築するために、dirname
とfilename
の間に明示的にパス区切り文字/
を挿入する必要があります。 - この変更により、
lstat
に渡されるパスは常に"dirname/filename"
の形式になります。
この変更は、dirname
変数の内容を不必要に再構築するのを避け、パス構築をlstat
呼び出しの直前で一度に行うことで、全体的な効率を向上させています。特に、readdir
が頻繁に呼び出されるようなI/Oヘビーなアプリケーションにおいて、わずかながらもパフォーマンス上のメリットが期待できます。
関連リンク
- Go言語の
os
パッケージのドキュメント: https://pkg.go.dev/os - Go言語の
strings
パッケージのドキュメント: https://pkg.go.dev/strings - Go言語の
filepath
パッケージのドキュメント: https://pkg.go.dev/path/filepath
参考にした情報源リンク
- Go String Concatenation Performance Optimization:
- Go
os.ReadDir
andfilepath.WalkDir
(Go 1.16以降の効率的なディレクトリ読み取り): - Go
filepath.Join
vsstrings.Builder
for path construction: