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

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

このコミットは、Go言語の標準ライブラリである path および path/filepath パッケージにおける Clean 関数のパフォーマンス改善を目的としています。具体的には、既に正規化(クリーンアップ)されたパスに対して Clean 関数が再度呼び出された際に発生する不要なメモリ割り当てを削減します。この最適化は、特にパス操作が頻繁に行われるアプリケーションにおいて、ガベージコレクションの負荷を軽減し、全体的なパフォーマンスを向上させる効果があります。

コミット

commit 9525372e31f6bad979a5f472aecfc24af34f28d0
Author: Russ Cox <rsc@golang.org>
Date:   Wed Jun 27 16:52:36 2012 -0400

    path/filepath: avoid allocation in Clean of cleaned path
    
    Alternative to https://golang.org/cl/6330044.
    
    Fixes #3681.
    
    R=golang-dev, r, hanwen, iant
    CC=golang-dev
    https://golang.org/cl/6335056

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

https://github.com/golang/go/commit/9525372e31f6bad979a5f472aecfc24af34f28d0

元コミット内容

path/filepath: avoid allocation in Clean of cleaned path

このコミットは、既にクリーンアップされたパスに対して Clean 関数が呼び出された際に、メモリ割り当てを避けるための変更です。これは、以前の変更提案 https://golang.org/cl/6330044 の代替案として提出されました。

この変更は、Issue #3681 を修正します。

変更の背景

Go言語の path.Clean および path/filepath.Clean 関数は、パス文字列を正規化(例: a/./ba/b に、a/../bb に)するために使用されます。しかし、これらの関数には、既に正規化されたパスが入力として与えられた場合でも、内部で新しいバイトスライス([]byte)を割り当ててしまうという非効率性がありました。

この不要なメモリ割り当ては、特にパス操作が頻繁に行われるようなシナリオ(例: ファイルシステムの走査、URLの処理、設定ファイルの解析など)において、ガベージコレクションの頻度を増加させ、アプリケーション全体のパフォーマンスに悪影響を与える可能性がありました。

Issue #3681 はこの問題点を指摘しており、このコミットはその解決策として提案されました。目標は、入力パスが既にクリーンな形式である場合に、Clean 関数が追加のメモリ割り当てを行わないようにすることでした。これにより、パフォーマンスの向上とガベージコレクションの負荷軽減が期待されます。

前提知識の解説

Go言語のパス操作パッケージ (pathpath/filepath)

  • path パッケージ: 主にスラッシュ (/) を区切り文字とするUnix形式のパスを操作するための関数を提供します。WebアプリケーションのURLパスや、クロスプラットフォームで統一されたパス表現を扱う際に利用されます。
  • path/filepath パッケージ: オペレーティングシステム固有のパス区切り文字(Windowsではバックスラッシュ \、Unix系ではスラッシュ /)を考慮したパス操作関数を提供します。ファイルシステム上の実際のパスを扱う際に使用されます。

両パッケージともに Clean 関数を提供しており、その基本的なロジックは似ています。

Clean 関数の役割

Clean 関数は、パス文字列を「最短かつ最もクリーンな」形式に正規化します。具体的には以下のルールを適用します。

  1. 連続するスラッシュを1つにまとめる (a//b -> a/b)
  2. . (カレントディレクトリ) を削除する (a/./b -> a/b)
  3. .. (親ディレクトリ) を解決する (a/b/../c -> a/c)
  4. パスが空の場合、. を返す
  5. ルートパス (/) は / のまま

メモリ割り当てとガベージコレクション (GC)

Go言語はガベージコレクタを持つ言語であり、開発者が明示的にメモリを解放する必要はありません。しかし、プログラムが頻繁に小さなオブジェクトを割り当てると、ガベージコレクタが頻繁に動作し、その分CPU時間を消費してしまいます。これは「GCのオーバーヘッド」と呼ばれ、パフォーマンスのボトルネックとなることがあります。

このコミットの目的は、不要なメモリ割り当てを避けることで、GCの頻度と負荷を減らし、パフォーマンスを向上させることにあります。

技術的詳細

このコミットの核心は、Clean 関数がパスを処理する際に、新しいバイトスライスを無条件に割り当てるのではなく、必要になるまで割り当てを遅延させる「遅延バッファ (lazy buffer)」の概念を導入した点にあります。

具体的には、lazybuf という新しい構造体が導入されました。

type lazybuf struct {
	s   string // 元の入力文字列
	buf []byte // 必要になった場合にのみ割り当てられるバイトスライス
	w   int    // buf または s の書き込み位置(現在のパスの長さ)
}

lazybuf は以下のメソッドを持ちます。

  • index(i int) byte: buf が存在すれば buf[i] を、そうでなければ s[i] を返す。これにより、元の文字列または新しく割り当てられたバッファのどちらからでもバイトを読み取ることができる。
  • append(c byte): バイト c をパスに追加する。
    • もし buf がまだ割り当てられておらず、かつ追加しようとしているバイト c が元の文字列 s の現在の位置 b.w と一致する場合、buf の割り当てをスキップし、b.w をインクリメントするだけで済ませる。これは、入力パスが既にクリーンな形式であり、変更が不要な場合にメモリ割り当てを避けるための重要な最適化。
    • もし buf がまだ割り当てられていないが、cs[b.w] と異なる場合(つまり、パスが変更される必要がある場合)、s の内容をコピーして buf を割り当て、その後に c を追加する。
    • buf が既に割り当てられている場合は、単に buf[b.w]c を追加する。
  • string() string: 最終的なパス文字列を返す。buf が存在すれば string(b.buf[:b.w]) を、そうでなければ b.s[:b.w] を返す。

Clean 関数は、パスの処理中に直接バイトスライスを操作する代わりに、この lazybufappend メソッドを通じてパスを構築するように変更されました。これにより、入力パスが既にクリーンな形式である場合、lazybuf は内部の buf を一度も割り当てることなく処理を完了し、元の文字列のスライスを返すことができます。これにより、不要なメモリ割り当てが完全に回避されます。

テストコードでは、runtime.MemStats を使用してメモリ割り当ての数を計測し、既にクリーンアップされたパスに対して Clean を呼び出した際に、割り当てがゼロになることを確認しています。

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

主に src/pkg/path/filepath/path.gosrc/pkg/path/path.goClean 関数、および関連するテストファイルが変更されています。

src/pkg/path/filepath/path.go (同様の変更が src/pkg/path/path.go にも適用)

追加された構造体とメソッド:

// A lazybuf is a lazily constructed path buffer.
// It supports append, reading previously appended bytes,
// and retrieving the final string. It does not allocate a buffer
// to hold the output until that output diverges from s.
type lazybuf struct {
	s   string
	buf []byte
	w   int
}

func (b *lazybuf) index(i int) byte {
	if b.buf != nil {
		return b.buf[i]
	}
	return b.s[i]
}

func (b *lazybuf) append(c byte) {
	if b.buf == nil {
		if b.w < len(b.s) && b.s[b.w] == c {
			b.w++
			return
		}
		b.buf = make([]byte, len(b.s))
		copy(b.buf, b.s[:b.w])
	}
	b.buf[b.w] = c
	b.w++
}

func (b *lazybuf) string() string {
	if b.buf == nil {
		return b.s[:b.w]
	}
	return string(b.buf[:b.w])
}

Clean 関数の変更:

変更前:

func Clean(path string) string {
	// ...
	buf := []byte(path) // ここで常に新しいバイトスライスが割り当てられていた
	r, w, dotdot := 0, 0, 0
	if rooted {
		buf[0] = Separator
		r, w, dotdot = 1, 1, 1
	}
	// ... buf を直接操作
	return FromSlash(vol + string(buf[0:w]))
}

変更後:

func Clean(path string) string {
	// ...
	out := lazybuf{s: path} // lazybuf を初期化
	r, dotdot := 0, 0
	if rooted {
		out.append(Separator) // append メソッドを使用
		r, dotdot = 1, 1
	}
	// ... out.append() や out.w を使用してパスを構築
	// 例: buf[w] = Separator -> out.append(Separator)
	// 例: w-- -> out.w--
	// 例: buf[w] -> out.index(out.w)
	// ...
	return FromSlash(vol + out.string()) // lazybuf の string() メソッドで最終文字列を取得
}

src/pkg/path/filepath/path_test.go (同様の変更が src/pkg/path/path_test.go にも適用)

メモリ割り当てをテストするコードの追加:

	var ms runtime.MemStats
	runtime.ReadMemStats(&ms)
	allocs := -ms.Mallocs
	const rounds = 100
	for i := 0; i < rounds; i++ {
		for _, test := range tests {
			filepath.Clean(test.result) // 既にクリーンなパスをテスト
		}
	}
	runtime.ReadMemStats(&ms)
	allocs += ms.Mallocs
	if allocs >= rounds {
		t.Errorf("Clean cleaned paths: %d allocations per test round, want zero", allocs/rounds)
	}

このテストは、Clean 関数が既にクリーンなパスに対して呼び出された場合に、allocs (割り当てられたメモリブロックの数) が rounds (テストの繰り返し回数) よりも小さい、理想的にはゼロであることを検証します。

コアとなるコードの解説

このコミットの主要な変更点は、Clean 関数がパスを構築する際に、直接バイトスライスを操作するのではなく、lazybuf という新しいヘルパー型を使用するように変更されたことです。

  1. lazybuf の導入:

    • lazybuf は、元の入力文字列 s と、必要になった場合にのみ割り当てられるバイトスライス buf を保持します。
    • w は、現在のパスの長さ、または buf (あるいは s) のどこまでが有効なパスデータであるかを示すインデックスです。
  2. 遅延割り当てのメカニズム (append メソッド):

    • append(c byte) メソッドが lazybuf の核心です。
    • パスに新しいバイト c を追加しようとするとき、まず b.buf == nil (まだバッファが割り当てられていない) かどうかをチェックします。
    • もし b.bufnil で、かつ b.w < len(b.s) && b.s[b.w] == c (追加しようとしているバイトが元の文字列の現在の位置のバイトと一致する) ならば、これはパスが変更されていないことを意味します。この場合、b.w をインクリメントするだけで、buf の割り当てをスキップします。これがメモリ割り当てを避けるための重要なポイントです。
    • もし b.bufnil で、かつ b.s[b.w] != c (パスが変更される必要がある) ならば、make([]byte, len(b.s)) で新しいバイトスライス buf を割り当て、元の文字列 s の内容を buf にコピーします。その後、cbuf[b.w] に追加します。
    • b.buf が既に割り当てられている場合は、単に buf[b.w]c を追加します。
  3. Clean 関数での利用:

    • Clean 関数は、out := lazybuf{s: path} として lazybuf のインスタンスを初期化します。
    • パスの正規化処理中、以前は buf[w] = ... のように直接バイトスライスに書き込んでいた箇所が、out.append(...) の呼び出しに置き換えられます。
    • パスの長さを追跡していた w 変数は、out.w に置き換えられます。
    • 最終的な結果文字列は、out.string() を呼び出すことで取得されます。これにより、buf が割り当てられていなければ元の文字列のスライスが、割り当てられていれば buf の内容が文字列として返されます。

この変更により、Clean 関数は、入力パスが既に正規化されている場合には、新しいメモリを割り当てることなく、元の文字列のスライスを返すことができるようになりました。これにより、ガベージコレクションの負荷が軽減され、パフォーマンスが向上します。

関連リンク

  • Go言語の path パッケージ: https://pkg.go.dev/path
  • Go言語の path/filepath パッケージ: https://pkg.go.dev/path/filepath
  • Go Issue #3681: path/filepath: Clean allocates even when path is already clean (このコミットが修正したIssue) - 検索結果から確認できます。

参考にした情報源リンク

  • Go言語の公式ドキュメント (path, path/filepath パッケージ)
  • Go言語のIssueトラッカー (Issue #3681)
  • Go言語のコードレビューシステム (Gerrit) の変更セット https://golang.org/cl/6335056 および https://golang.org/cl/6330044 (コミットメッセージに記載)
  • Go言語のメモリ管理とガベージコレクションに関する一般的な情報源