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

[インデックス 15241] ファイルの概要

このコミットは、Go言語の標準ライブラリosパッケージ内のGetwd関数(現在の作業ディレクトリを取得する関数)のパフォーマンス改善を目的としています。具体的には、Getwd関数の結果をキャッシュすることで、同じディレクトリに対する繰り返しの呼び出しにおいて、コストの高いパス解決アルゴリズムの実行を回避します。

コミット

commit 89fde30fbda9a86a7b53db7b7cee1cfd8e1a36ff
Author: Russ Cox <rsc@golang.org>
Date:   Thu Feb 14 14:21:09 2013 -0500

    os: cache Getwd result as hint for next time
    
    Avoids the dot-dot-based algorithm on repeated calls
    when the directory hasn't changed.
    
    R=golang-dev, iant, bradfitz
    CC=golang-dev
    https://golang.org/cl/7340043

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/89fde30fbda9a86a7b53db7b7cee1cfd8e1a36ff

元コミット内容

os: cache Getwd result as hint for next time

このコミットは、os.Getwd関数の結果を次回の呼び出しのためのヒントとしてキャッシュします。これにより、ディレクトリが変更されていない場合の繰り返しの呼び出しで、ドットドット(..)ベースのアルゴリズムの実行を回避します。

変更の背景

os.Getwd()関数は、現在の作業ディレクトリの絶対パスを返します。この処理は、特にUnix系システムにおいて、現在のディレクトリからルートディレクトリまで親ディレクトリを辿っていく(..を解決していく)ことで絶対パスを構築する、比較的コストの高い操作となる場合があります。

アプリケーションが頻繁にos.Getwd()を呼び出す場合、例えば、複数の処理で現在の作業ディレクトリのパスが必要とされるようなシナリオでは、このコストが累積し、パフォーマンスのボトルネックとなる可能性がありました。ディレクトリが変更されていないにもかかわらず、毎回同じ高コストな処理を実行することは非効率です。

このコミットの背景には、このような非効率性を解消し、os.Getwd()の呼び出しコストを削減することで、Goプログラム全体のパフォーマンスを向上させるという目的があります。特に、ディレクトリが頻繁に変わらない状況での連続した呼び出しにおいて、その効果が顕著に現れることが期待されます。

前提知識の解説

1. os.Getwd()関数

Go言語のosパッケージに含まれるGetwd()関数は、現在の作業ディレクトリの絶対パスを文字列として返します。エラーが発生した場合はエラー情報も返します。

func Getwd() (dir string, err error)

この関数は、シェルでpwdコマンドを実行するのと同様の機能を提供します。

2. 現在の作業ディレクトリの解決メカニズム

Unix系OSにおいて、現在の作業ディレクトリの絶対パスを特定する一般的な方法は、以下のステップを含みます。

  • カレントディレクトリのinodeの取得: まず、現在のディレクトリ(.)のファイルシステム上のinode(ファイルやディレクトリを一意に識別する番号)を取得します。
  • 親ディレクトリへの遡上: 次に、親ディレクトリ(..)を辿りながら、各ディレクトリの名前とinodeを解決していきます。このプロセスは、ルートディレクトリ(/)に到達するまで繰り返されます。
  • パスの構築: 遡上中に収集したディレクトリ名を逆順に結合することで、絶対パスを構築します。

この「ドットドット(..)ベースのアルゴリズム」は、特にディレクトリの階層が深い場合や、シンボリックリンクが絡む場合に、複数のファイルシステムアクセス(statシステムコールなど)を伴うため、CPU時間やI/Oリソースを消費する可能性があります。

3. キャッシュの概念

キャッシュとは、計算コストの高い処理の結果を一時的に保存しておき、同じ入力に対しては再計算せずに保存された結果を返すことで、処理速度を向上させる技術です。

  • メリット:
    • パフォーマンス向上: 繰り返しの計算やI/Oを削減し、応答時間を短縮します。
    • リソース消費の削減: CPUやディスクI/Oなどのリソース使用量を減らします。
  • デメリット:
    • メモリ消費: キャッシュされたデータを保存するためにメモリを消費します。
    • キャッシュの一貫性: 元データが変更された場合に、キャッシュされたデータが古くなる(Stale Cache)問題が発生する可能性があります。これを解決するためには、キャッシュの無効化(Invalidation)戦略が必要です。

このコミットでは、os.Getwd()の結果をキャッシュし、キャッシュされたパスが現在のディレクトリと同一であるかを検証することで、キャッシュの一貫性を保っています。

4. sync.Mutex

Go言語のsyncパッケージにあるMutex(ミューテックス)は、共有リソースへのアクセスを排他的に制御するための同期プリミティブです。複数のゴルーチンが同時に共有データにアクセスしようとすると、データ競合(Data Race)が発生し、プログラムの動作が予測不能になる可能性があります。Mutexを使用することで、一度に一つのゴルーチンのみが共有データにアクセスできるようにし、データ競合を防ぎます。

  • Lock(): ミューテックスをロックします。既にロックされている場合は、ロックが解放されるまでゴルーチンはブロックされます。
  • Unlock(): ミューテックスをアンロックします。

このコミットでは、getwdCacheというグローバルなキャッシュ変数へのアクセスを保護するためにsync.Mutexが使用されており、複数のゴルーチンからのGetwd()呼び出しが同時に発生しても、キャッシュデータが安全に読み書きされることを保証しています。

5. os.Stat()os.SameFile()

  • os.Stat(name string): 指定されたパスのファイル情報を返します。これには、ファイルの種類、サイズ、パーミッション、更新時刻、そしてファイルシステム上のinode情報などが含まれます。
  • os.SameFile(fi1, fi2 FileInfo): 2つのFileInfoインターフェースが同じファイルを参照しているかどうかを報告します。これは通常、ファイルシステム上のデバイスIDとinode番号を比較することで行われます。これにより、パスが異なっていても同じ物理ファイルを参照しているかどうかを判断できます(例: ハードリンクやシンボリックリンクの解決後)。

このコミットでは、キャッシュされたパスが現在の作業ディレクトリと本当に同じであるかを検証するために、Stat()でファイル情報を取得し、SameFile()で比較しています。これにより、キャッシュされたパスがシンボリックリンクの解決などによって現在のディレクトリと異なる実体を参照している場合に、誤ったキャッシュを使用することを防ぎます。

技術的詳細

このコミットは、os.Getwd()関数の内部にシンプルなキャッシュ機構を導入することで、パフォーマンスを向上させています。

  1. キャッシュ構造の定義: src/pkg/os/getwd.goの冒頭に、getwdCacheというグローバル変数が追加されました。この構造体は、キャッシュされたディレクトリパスを保持するdirフィールドと、並行アクセスからdirを保護するためのsync.Mutexを含んでいます。

    var getwdCache struct {
    	sync.Mutex
    	dir string
    }
    

    sync.Mutexを使用することで、複数のゴルーチンが同時にGetwd()を呼び出した場合でも、キャッシュの読み書きが安全に行われ、データ競合が防止されます。

  2. キャッシュの利用: Getwd()関数の冒頭で、まずgetwdCacheからキャッシュされたパス(pwd)を読み取ります。

    	getwdCache.Lock()
    	pwd = getwdCache.dir
    	getwdCache.Unlock()
    	if len(pwd) > 0 { // キャッシュが存在する場合
    		d, err := Stat(pwd) // キャッシュされたパスのファイル情報を取得
    		if err == nil && SameFile(dot, d) { // キャッシュされたパスが現在のディレクトリと同一であるか検証
    			return pwd, nil // 同一であればキャッシュされたパスを返す
    		}
    	}
    

    キャッシュされたパスが存在し(len(pwd) > 0)、かつそのパスが現在の作業ディレクトリ(dot、これはGetwd()の内部で現在のディレクトリのFileInfoとして取得される)とSameFile()によって同一であると確認できた場合、高コストなパス解決処理をスキップして、キャッシュされたパスを即座に返します。このSameFileチェックは、キャッシュの鮮度と正確性を保証するために非常に重要です。例えば、cdコマンドなどでディレクトリが変更された場合や、シンボリックリンクの解決によってパスの実体が変化した場合でも、古いキャッシュが誤って使用されることを防ぎます。

  3. キャッシュの更新: Getwd()関数が現在の作業ディレクトリのパスを正常に解決した後、その結果をgetwdCacheに保存します。

    	getwdCache.Lock()
    	getwdCache.dir = pwd
    	getwdCache.Unlock()
    

    これにより、次回のGetwd()呼び出し時に、この新しいパスがキャッシュとして利用可能になります。この更新もsync.Mutexによって保護されており、安全な書き込みが保証されます。

このキャッシュ戦略は、「ヒント」として機能します。つまり、キャッシュされた値が常に正しいとは限らないため、StatSameFileによる検証ステップが不可欠です。しかし、ほとんどの場合、ディレクトリは頻繁に変わらないため、この検証ステップは成功し、高コストな「ドットドットベースのアルゴリズム」の実行を大幅に削減できます。

コアとなるコードの変更箇所

変更はsrc/pkg/os/getwd.goファイルに集中しています。

--- a/src/pkg/os/getwd.go
+++ b/src/pkg/os/getwd.go
@@ -5,9 +5,15 @@
 package os
 
 import (
+	"sync"
 	"syscall"
 )
 
+var getwdCache struct {
+	sync.Mutex
+	dir string
+}
+
 // Getwd returns a rooted path name corresponding to the
 // current directory.  If the current directory can be
 // reached via multiple paths (due to symbolic links),
@@ -35,6 +41,17 @@ func Getwd() (pwd string, err error) {
 		}
 	}\n
+\t// Apply same kludge but to cached dir instead of $PWD.
+\tgetwdCache.Lock()
+\tpwd = getwdCache.dir
+\tgetwdCache.Unlock()
+\tif len(pwd) > 0 {
+\t\td, err := Stat(pwd)
+\t\tif err == nil && SameFile(dot, d) {
+\t\t\treturn pwd, nil
+\t\t}\n+\t}\n+\n \t// Root is a special case because it has no parent
 \t// and ends in a slash.\n \troot, err := Stat("/")
@@ -88,5 +105,11 @@ func Getwd() (pwd string, err error) {
 \t\t// Set up for next round.\n \t\tdot = pd
 \t}\n+\n+\t// Save answer as hint to avoid the expensive path next time.
+\tgetwdCache.Lock()
+\tgetwdCache.dir = pwd
+\tgetwdCache.Unlock()\n+\n \treturn pwd, nil
 }\n

コアとなるコードの解説

  1. import "sync" の追加: キャッシュの並行アクセスを安全にするために、syncパッケージがインポートされました。

  2. getwdCache グローバル変数の定義:

    var getwdCache struct {
    	sync.Mutex
    	dir string
    }
    

    getwdCacheは、Getwd()関数が共有するグローバルなキャッシュ変数です。

    • sync.Mutex: キャッシュの読み書き時にデータ競合を防ぐためのミューテックス。
    • dir string: キャッシュされた現在の作業ディレクトリのパスを保持する文字列。
  3. キャッシュの利用ロジックの追加: Getwd()関数の既存のロジックの前に、以下のコードブロックが追加されました。

    	getwdCache.Lock()
    	pwd = getwdCache.dir
    	getwdCache.Unlock()
    	if len(pwd) > 0 {
    		d, err := Stat(pwd)
    		if err == nil && SameFile(dot, d) {
    			return pwd, nil
    		}
    	}
    
    • まずgetwdCache.Lock()getwdCache.Unlock()でミューテックスをロック・アンロックし、getwdCache.dirからキャッシュされたパスを安全に読み取ります。
    • if len(pwd) > 0で、キャッシュに有効なパスが格納されているかを確認します。
    • d, err := Stat(pwd)で、キャッシュされたパスのファイル情報を取得します。
    • if err == nil && SameFile(dot, d)で、エラーがなく、かつキャッシュされたパスが現在のディレクトリ(dot変数に格納されている現在のディレクトリのFileInfo)とファイルシステム上で同一であるかを検証します。SameFileはinode番号などを比較するため、シンボリックリンクなどによってパス名が異なっても同じ実体を指していればtrueを返します。
    • これらの条件がすべて満たされた場合、キャッシュされたpwdを即座に返し、高コストなパス解決処理をスキップします。
  4. キャッシュの更新ロジックの追加: Getwd()関数の末尾、return pwd, nilの直前に以下のコードブロックが追加されました。

    	getwdCache.Lock()
    	getwdCache.dir = pwd
    	getwdCache.Unlock()
    

    Getwd()関数が現在の作業ディレクトリのパス(pwd)を正常に決定した後、その結果をgetwdCache.dirに保存します。これもミューテックスで保護されており、次回の呼び出しでこのキャッシュが利用できるようになります。

これらの変更により、os.Getwd()は、ディレクトリが変更されていない限り、初回呼び出し以降はキャッシュを利用して高速にパスを返すことができるようになりました。

関連リンク

参考にした情報源リンク

  • Go言語の公式ドキュメント
  • Go言語のソースコードリポジトリ
  • 一般的なオペレーティングシステムの「現在の作業ディレクトリ」解決メカニズムに関する情報