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

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

コミット

このコミットは、Go言語の標準ライブラリbytesパッケージ内のlastIndexFunc関数に対するマイナーな最適化を導入しています。具体的には、TrimSpaceなどの空白文字をトリムする操作において、ASCII文字の処理を高速化することでパフォーマンスを向上させています。ベンチマーク結果として、BenchmarkTrimSpaceの実行時間が大幅に短縮されたことが示されています(81.3 ns/op から 58.0 ns/op へ)。この最適化は、lastIndexFuncがバイトスライスを逆順に走査する際に、UTF-8デコードのコストを削減することを目的としています。

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

https://github.com/golang/go/commit/30a9957aacb21fef5195a43dba99669465a789e7

元コミット内容

bytes: minor optimization to lastIndexFunc

Before and after:
BenchmarkTrimSpace  20000000   81.3 ns/op
BenchmarkTrimSpace  50000000   58.0 ns/op

(most whitespace trimming is ASCII whitespace)

Same optimization appeared a handful of other places
in this file, but not here.

R=golang-dev, dave
CC=golang-dev
https://golang.org/cl/7305063

変更の背景

この変更の背景には、Go言語の標準ライブラリにおけるパフォーマンスの継続的な改善という目標があります。特に、bytesパッケージは文字列やバイトスライスの操作において頻繁に使用されるため、その中の関数のわずかな最適化でも、全体的なアプリケーションのパフォーマンスに大きな影響を与える可能性があります。

コミットメッセージに明記されているように、TrimSpace関数(バイトスライスから先頭および末尾の空白文字を削除する関数)のベンチマーク結果が改善されています。これは、多くの空白文字のトリミング操作がASCII空白文字(スペース、タブ、改行など)を対象としているため、これらの文字の処理を効率化することが重要であるという認識に基づいています。

lastIndexFuncは、バイトスライスを逆順に走査し、指定された条件を満たすルーン(Unicodeコードポイント)の最後のインデックスを見つける汎用関数です。TrimSpaceのような関数は、このlastIndexFuncを利用して、末尾の空白文字の位置を特定します。したがって、lastIndexFuncのパフォーマンス改善は、TrimSpaceのパフォーマンスに直接寄与します。

この最適化は、同じファイル内の他の場所で同様の最適化が既に適用されていたにもかかわらず、lastIndexFuncにはまだ適用されていなかったという状況を是正するものです。これにより、コードベース全体の一貫性と効率性が向上します。

前提知識の解説

Go言語のbytesパッケージ

bytesパッケージは、バイトスライス([]byte型)を操作するためのユーティリティ関数を提供します。これは、文字列(string型)が不変であるのに対し、バイトスライスは可変であるため、効率的なデータ操作が必要な場合に特に有用です。ファイルI/O、ネットワーク通信、バイナリデータの処理など、幅広い用途で利用されます。

bytes.TrimSpace関数

bytes.TrimSpace(s []byte) []byteは、バイトスライスsの先頭と末尾からすべてのUnicode空白文字を削除した新しいバイトスライスを返します。内部的には、bytes.IndexFuncbytes.LastIndexFuncを使用して、トリムすべき範囲を特定します。

bytes.lastIndexFunc関数

func lastIndexFunc(s []byte, f func(r rune) bool, truth bool) intは、バイトスライスsを末尾から先頭に向かって走査し、関数ftruthと一致する最初のルーンの開始インデックスを返します。この関数は、特定の条件を満たす最後の文字の位置を見つけるために使用されます。例えば、TrimSpaceでは、この関数を使って末尾の空白文字の最後の位置を特定します。

unicode/utf8パッケージ

Go言語では、文字列はUTF-8エンコードされたバイトのシーケンスとして扱われます。unicode/utf8パッケージは、UTF-8エンコードされたバイトスライスからルーン(Unicodeコードポイント)をデコードしたり、ルーンをUTF-8バイトにエンコードしたりするための関数を提供します。

  • utf8.DecodeLastRune(p []byte) (r rune, size int): バイトスライスpの末尾からUTF-8エンコードされたルーンをデコードします。デコードされたルーンと、そのルーンが占めるバイト数を返します。この関数は、可変長であるUTF-8の特性上、ルーンの開始位置を正確に特定するために、バイトスライスを逆順に走査する際に必要となります。
  • utf8.RuneSelf: この定数は、ASCII文字の最大値(127)を表します。つまり、r < utf8.RuneSelfであるルーンrはASCII文字であり、1バイトで表現されます。r >= utf8.RuneSelfであるルーンは、ASCII範囲外の文字であり、UTF-8では1バイト以上(最大4バイト)で表現されます。

ルーン (Rune)

Go言語におけるrune型は、Unicodeコードポイントを表す組み込みのエイリアス型です(実体はint32)。Goの文字列はUTF-8でエンコードされたバイトのシーケンスですが、個々の文字を扱う際にはrune型が使用されます。

技術的詳細

この最適化の核心は、lastIndexFuncがバイトスライスを逆順に走査する際に、ASCII文字のデコード処理を高速化することにあります。

元のlastIndexFuncの実装では、ループの各イテレーションでutf8.DecodeLastRune(s[0:i])を呼び出して、現在の位置iの直前のルーンをデコードしていました。utf8.DecodeLastRuneは、UTF-8の可変長エンコーディングに対応するため、ルーンの開始位置とサイズを正確に判断するために、バイトスライスの末尾からバイトを読み取り、複雑なロジックを実行します。これは、特にASCII文字のように常に1バイトで表現される文字に対しても、そのオーバーヘッドが発生していました。

変更後のコードでは、以下の最適化が導入されています。

func lastIndexFunc(s []byte, f func(r rune) bool, truth bool) int {
	for i := len(s); i > 0; {
		r, size := rune(s[i-1]), 1 // まず最後のバイトをルーンとして扱い、サイズを1と仮定
		if r >= utf8.RuneSelf {    // そのルーンがASCII範囲外(127より大きい)の場合
			r, size = utf8.DecodeLastRune(s[0:i]) // 正しくUTF-8デコードを行う
		}
		i -= size
		if f(r) == truth {
			return i
		}
	}
	return -1
}

この変更のポイントは、if r >= utf8.RuneSelfという条件分岐です。

  1. ASCII文字の高速パス:

    • まず、s[i-1](現在の位置の直前のバイト)を直接rune型にキャストし、そのルーンのサイズを1と仮定します。これは、ASCII文字が常に1バイトで表現されるという事実に基づいています。
    • 次に、r >= utf8.RuneSelfという条件で、このルーンがASCII範囲内(0-127)であるかどうかをチェックします。
    • もしr < utf8.RuneSelf(つまりASCII文字)であれば、そのルーンは1バイトであることが確定しているため、utf8.DecodeLastRuneを呼び出すことなく、rsizeがそのまま使用されます。これにより、utf8.DecodeLastRuneの呼び出しに伴うオーバーヘッドが回避され、処理が高速化されます。
  2. 非ASCII文字の通常パス:

    • もしr >= utf8.RuneSelf(つまり非ASCII文字)であれば、そのルーンは1バイト以上で表現される可能性があるため、utf8.DecodeLastRune(s[0:i])を呼び出して、正確なルーンとサイズをデコードします。この場合、元の実装と同じロジックが適用されます。

この最適化は、TrimSpaceのような関数が主にASCII空白文字を処理する場合に特に効果を発揮します。コミットメッセージにもあるように、「ほとんどの空白文字のトリミングはASCII空白文字である」ため、この高速パスが頻繁に利用され、全体的なパフォーマンスが向上します。ベンチマーク結果の改善(81.3 ns/op から 58.0 ns/op)は、この最適化が実測で効果的であることを示しています。

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

src/pkg/bytes/bytes.go ファイルの lastIndexFunc 関数が変更されています。

--- a/src/pkg/bytes/bytes.go
+++ b/src/pkg/bytes/bytes.go
@@ -571,7 +571,10 @@ func indexFunc(s []byte, f func(r rune) bool, truth bool) int {
 // inverted.
 func lastIndexFunc(s []byte, f func(r rune) bool, truth bool) int {
 	for i := len(s); i > 0; {
-		r, size := utf8.DecodeLastRune(s[0:i])
+		r, size := rune(s[i-1]), 1
+		if r >= utf8.RuneSelf {
+			r, size = utf8.DecodeLastRune(s[0:i])
+		}
 		i -= size
 		if f(r) == truth {
 			return i

また、src/pkg/bytes/bytes_test.goBenchmarkTrimSpace が追加されています。

--- a/src/pkg/bytes/bytes_test.go
+++ b/src/pkg/bytes/bytes_test.go
@@ -1073,3 +1073,10 @@ func BenchmarkFieldsFunc(b *testing.B) {
 		FieldsFunc(fieldsInput, unicode.IsSpace)
 	}
 }
+
+func BenchmarkTrimSpace(b *testing.B) {
+	s := []byte("  Some text.  \n")
+	for i := 0; i < b.N; i++ {
+		TrimSpace(s)
+	}
+}

コアとなるコードの解説

変更されたlastIndexFuncのコア部分は以下の通りです。

		r, size := rune(s[i-1]), 1
		if r >= utf8.RuneSelf {
			r, size = utf8.DecodeLastRune(s[0:i])
		}
  1. r, size := rune(s[i-1]), 1:

    • s[i-1]は、現在の走査位置iの直前のバイトです。
    • このバイトを直接rune型にキャストし、変数rに代入します。
    • 同時に、そのルーンが占めるバイト数size1と仮定します。
    • この行は、まず現在のバイトがASCII文字である可能性を考慮した「仮の」デコード結果を設定します。
  2. if r >= utf8.RuneSelf:

    • ここで、rutf8.RuneSelf(ASCII文字の最大値127)以上であるかをチェックします。
    • もしrutf8.RuneSelf以上であれば、それはASCII範囲外の文字であり、UTF-8では1バイト以上で表現される可能性があります。この場合、最初の仮定(size1)は誤っているか、不十分であるため、正確なデコードが必要です。
  3. r, size = utf8.DecodeLastRune(s[0:i]):

    • if条件が真の場合(非ASCII文字の場合)、utf8.DecodeLastRune関数が呼び出されます。
    • この関数は、バイトスライスs[0:i]の末尾から正確なルーンと、そのルーンが占めるバイト数をデコードして返します。これにより、非ASCII文字が正しく処理されます。

このロジックにより、lastIndexFuncは、処理対象のバイトがASCII文字である場合にはutf8.DecodeLastRuneの呼び出しをスキップし、非ASCII文字である場合にのみ呼び出すことで、パフォーマンスを向上させています。これは、Go言語の標準ライブラリでよく見られる、一般的なケース(この場合はASCII文字)を高速化するための最適化パターンの一つです。

また、bytes_test.goに追加されたBenchmarkTrimSpaceは、この最適化の効果を測定するためのベンチマークテストです。TrimSpace関数が内部でlastIndexFuncを使用しているため、lastIndexFuncの最適化がTrimSpaceのパフォーマンスに直接反映されることを確認できます。

関連リンク

参考にした情報源リンク