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

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

このコミットは、Go言語の標準ライブラリ bytes パッケージにおける CountIndexEqual の各関数のパフォーマンスを向上させることを目的としています。特に、Equal 関数にはアセンブリ言語による最適化が導入され、Count および Index 関数には、一般的なケース("easy" case)での検索効率を大幅に向上させるためのアルゴリズム変更が加えられました。これにより、特定の操作において顕著な速度向上が見られます。

コミット

commit 9b875bc037407b47c4922871390fbae8e3f16592
Author: Russ Cox <rsc@golang.org>
Date:   Wed Dec 7 15:09:56 2011 -0500

    bytes: faster Count, Index, Equal
    
    Benchmarks are from GOARCH=amd64 on a MacPro5,1.
    
    benchmark                                    old MB/s     new MB/s  speedup
    bytes_test.BenchmarkEqual32                    452.89       891.07    1.97x
    bytes_test.BenchmarkEqual4K                    852.71      1700.44    1.99x
    bytes_test.BenchmarkEqual4M                    841.53      1587.93    1.89x
    bytes_test.BenchmarkEqual64M                   838.22      1578.14    1.88x
    
    bytes_test.BenchmarkIndex32                     58.02        48.99    0.84x
    bytes_test.BenchmarkIndex4K                     48.26        41.32    0.86x
    bytes_test.BenchmarkIndex4M                     48.20        41.24    0.86x
    bytes_test.BenchmarkIndex64M                    48.08        41.21    0.86x
    bytes_test.BenchmarkIndexEasy32                410.04       546.82    1.33x
    bytes_test.BenchmarkIndexEasy4K                849.26     14257.37   16.79x
    bytes_test.BenchmarkIndexEasy4M                854.54     17222.15   20.15x
    bytes_test.BenchmarkIndexEasy64M               843.57     11060.40   13.11x
    
    bytes_test.BenchmarkCount32                     57.24        50.68    0.89x
    bytes_test.BenchmarkCount4K                     48.19        41.82    0.87x
    bytes_test.BenchmarkCount4M                     48.18        41.74    0.87x
    bytes_test.BenchmarkCount64M                    48.17        41.71    0.87x
    bytes_test.BenchmarkCountEasy32                433.11       547.44    1.26x
    bytes_test.BenchmarkCountEasy4K               1130.59     14194.06   12.55x
    bytes_test.BenchmarkCountEasy4M               1131.23     17231.18   15.23x
    bytes_test.BenchmarkCountEasy64M              1111.40     11068.88    9.96x
    
    The non-easy Count/Index benchmarks are a worst case input.
    
    regexp.BenchmarkMatchEasy0_32                  237.46       221.47    0.93x
    regexp.BenchmarkMatchEasy0_1K                  553.53      1019.72    1.84x
    regexp.BenchmarkMatchEasy0_32K                 693.99      1672.06    2.41x
    regexp.BenchmarkMatchEasy0_1M                  688.72      1611.68    2.34x
    regexp.BenchmarkMatchEasy0_32M                 680.70      1565.05    2.30x
    regexp.BenchmarkMatchEasy1_32                  165.56       243.08    1.47x
    regexp.BenchmarkMatchEasy1_1K                  336.45       496.32    1.48x
    regexp.BenchmarkMatchEasy1_32K                 302.80       425.63    1.41x
    regexp.BenchmarkMatchEasy1_1M                  300.42       414.20    1.38x
    regexp.BenchmarkMatchEasy1_32M                 299.64       413.47    1.38x
    
    R=golang-dev, r, iant
    CC=golang-dev
    https://golang.org/cl/5451116

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

https://github.com/golang/go/commit/9b875bc037407b47c4922871390fbae8e3f16592

元コミット内容

bytes パッケージの CountIndexEqual 関数を高速化するコミット。ベンチマーク結果が示されており、Equal は約2倍、IndexCount の「イージーケース」は約10倍から20倍の速度向上を達成しています。一方で、「ノンイージーケース」(ワーストケース入力)ではわずかな速度低下が見られます。また、regexp パッケージのベンチマークも改善されています。

変更の背景

Go言語の標準ライブラリは、そのパフォーマンスが非常に重要視されています。特に bytes パッケージは、文字列やバイトスライスを扱う多くのアプリケーションで頻繁に使用されるため、これらの基本操作の効率はシステム全体のパフォーマンスに大きな影響を与えます。

このコミットの背景には、bytes.Equalbytes.Indexbytes.Count といった基本的なバイトスライス操作の実行速度をさらに向上させたいという明確な意図がありました。ベンチマーク結果が示すように、既存の実装ではまだ最適化の余地があり、特に大量のデータを扱う場合や、これらの関数が頻繁に呼び出されるシナリオにおいて、パフォーマンスのボトルネックとなる可能性がありました。

Equal 関数については、Go言語のポータブルな実装(Goで書かれたコード)に加えて、特定のアーキテクチャ(x86/amd64)向けにアセンブリ言語による最適化を導入することで、CPUの低レベルな命令を活用し、比較処理を極限まで高速化することが目指されました。

Index および Count 関数については、一般的な使用パターン(例えば、検索対象のバイトスライスが比較的短かったり、検索パターンがスライス内に早期に見つかる場合)において、より効率的なアルゴリズムを導入することで、大幅な速度向上を図ることが目的でした。同時に、ワーストケース(検索パターンがスライス内に存在しない、または非常に遅く見つかる場合)でのパフォーマンスへの影響も考慮し、全体的なバランスを取る必要がありました。

これらの最適化は、Go言語で書かれたアプリケーションが、より高速に、より少ないリソースで動作することを可能にし、特にネットワークプログラミング、データ処理、テキスト処理など、バイトスライス操作が中心となる分野でのGoの競争力を高めることに貢献します。

前提知識の解説

このコミットを理解するためには、以下の技術的な概念について基本的な知識があると役立ちます。

  1. Go言語の bytes パッケージ:

    • Go言語の標準ライブラリの一部で、バイトスライス([]byte)を操作するためのユーティリティ関数を提供します。
    • Equal(a, b []byte) bool: 2つのバイトスライスが等しいかどうかを比較します。
    • Index(s, sep []byte) int: s の中で sep が最初に出現するインデックスを返します。見つからない場合は -1 を返します。
    • Count(s, sep []byte) int: s の中で sep が重複せずに何回出現するかを数えます。
    • IndexByte(s []byte, c byte) int: s の中でバイト c が最初に出現するインデックスを返します。
  2. アセンブリ言語 (Assembly Language):

    • CPUが直接実行できる機械語命令を、人間が読み書きしやすいようにニーモニック(記号)で表現した低レベルプログラミング言語です。
    • 特定のCPUアーキテクチャ(例: x86, AMD64, ARM)に特化しており、ハードウェアの機能を最大限に引き出すために使用されます。
    • Go言語では、パフォーマンスがクリティカルな部分(例: ランタイム、標準ライブラリの特定の関数)でアセンブリ言語が使用されることがあります。
  3. x86/AMD64 アーキテクチャと REP CMPSB 命令:

    • IntelおよびAMDのCPUで広く使われている命令セットアーキテクチャです。
    • CMPSB (Compare String Byte): DS:SI が指すバイトと ES:DI が指すバイトを比較し、フラグレジスタを更新します。ポインタは自動的にインクリメント/デクリメントされます。
    • REP (Repeat Prefix): CMPSB などの文字列操作命令の前に付けるプレフィックスで、CX レジスタが0になるまで命令を繰り返します。これにより、ループ処理をCPUレベルで非常に高速に実行できます。REP CMPSB は、メモリブロックの比較(memcmp に相当)に非常に効率的です。
  4. Go言語のベンチマーク:

    • Go言語には、コードのパフォーマンスを測定するための組み込みのベンチマーク機能があります。
    • go test -bench=. コマンドで実行され、関数の実行時間やメモリ割り当てなどを測定し、スループット(MB/s)などの指標で結果を表示します。
    • ベンチマーク名に Easy が付いているものは、一般的な、または最適化が効きやすい入力パターンを指し、そうでないものはワーストケースやより複雑な入力パターンを指すことが多いです。
  5. ポータブルな実装とアセンブリ実装:

    • Go言語のコードは、異なるCPUアーキテクチャやOSで動作するように設計されています(ポータブル)。
    • しかし、特定のアーキテクチャでは、Goで書かれたポータブルなコードよりも、そのアーキテクチャに特化したアセンブリ言語で書かれたコードの方がはるかに高速に動作する場合があります。
    • このコミットでは、Equal 関数に対して、Goで書かれた equalPortable と、x86/amd64向けのアセンブリ実装が共存する形になっています。Goのコンパイラは、実行環境に応じて最適な実装を選択します。

これらの知識は、コミットがなぜ、どのようにパフォーマンスを向上させたのかを深く理解する上で不可欠です。

技術的詳細

このコミットにおけるパフォーマンス向上は、主に以下の2つのアプローチによって実現されています。

  1. bytes.Equal のアセンブリ言語による最適化:

    • x86 (386) および AMD64 アーキテクチャ: src/pkg/bytes/asm_386.ssrc/pkg/bytes/asm_amd64.s に、Equal 関数のアセンブリ実装が追加されました。
    • この実装の核となるのは、REP; CMPSB 命令の使用です。
      • REP (Repeat) プレフィックスは、続く命令を CX レジスタがゼロになるまで繰り返すことをCPUに指示します。
      • CMPSB (Compare String Byte) 命令は、DS:SI(ソース)と ES:DI(デスティネーション)が指すメモリ位置のバイトを比較し、結果に応じてCPUのフラグレジスタ(特にZF: Zero Flag)を設定します。比較後、SIDI レジスタは自動的にインクリメント(またはデクリメント)されます。
    • Equal 関数は、まず2つのバイトスライスの長さが異なる場合は false を返します。長さが同じ場合、REP; CMPSB を使用してバイトスライス全体を一度に比較します。この命令は、ループのオーバーヘッドをCPU内部で処理するため、GoのループやC言語の memcmp よりもさらに高速なバイト比較を可能にします。
    • ベンチマーク結果 (BenchmarkEqual*) は、このアセンブリ最適化により約1.9倍の速度向上を達成していることを明確に示しています。これは、CPUのネイティブな文字列比較命令を直接利用することの強力な効果です。
    • ARM アーキテクチャ: src/pkg/bytes/asm_arm.s では、Equal 関数が equalPortable(Goで書かれたポータブルな実装)に分岐するように変更されています。これは、当時のARMアーキテクチャにはx86/amd64のような効率的な memcmp に相当するアセンブリ命令がまだ利用できなかったか、実装が複雑であったためと考えられます。したがって、ARM環境では Equal のアセンブリ最適化は適用されず、Goのポータブルな実装が使用されます。
  2. bytes.Count および bytes.Index のアルゴリズム改善:

    • src/pkg/bytes/bytes.go 内の CountIndex 関数が書き直されました。
    • 「イージーケース」の最適化: 以前の実装では、検索パターン (sep) の最初のバイトが見つかるたびに、Equal 関数を呼び出して完全なパターンマッチングを行っていました。新しい実装では、IndexByte 関数(単一バイトの検索に特化しており、通常は非常に高速)を積極的に利用します。
      • Index 関数では、まず sep の最初のバイト (c) を IndexByte で検索します。これにより、sep の最初のバイトが見つかるまで、多くの不要な比較をスキップできます。
      • Count 関数でも同様に、IndexByte を使って sep の最初のバイトを効率的に探し、その位置から完全なパターンマッチングを試みます。
      • このアプローチにより、検索パターンが頻繁に出現しない場合や、検索対象のデータが非常に大きい場合でも、IndexByte が高速に次の検索開始位置を見つけるため、大幅なパフォーマンス向上が期待できます。
    • ベンチマーク結果の解釈:
      • BenchmarkIndexEasy* および BenchmarkCountEasy* のベンチマークでは、約10倍から20倍という驚異的な速度向上が見られます。これは、上記 IndexByte を活用したスキップ戦略が、検索パターンが比較的早期に見つかる「イージーケース」において非常に効果的であることを示しています。
      • 一方で、BenchmarkIndex* および BenchmarkCount*(「ノンイージーケース」またはワーストケース)では、わずかな速度低下が見られます。これは、検索パターンが全く見つからない場合や、部分的なマッチングが頻繁に発生するようなワーストケースでは、IndexByte によるスキップがほとんど機能せず、むしろ IndexByte の呼び出しオーバーヘッドが加わるためと考えられます。しかし、このわずかな低下は、「イージーケース」での大幅な向上によって十分に相殺されると判断されたのでしょう。
    • sep の長さが1の場合の最適化: Index 関数では、sep の長さが1の場合に IndexByte を直接呼び出すように最適化されています。これは、単一バイトの検索が最も一般的なケースの一つであり、これに特化した高速パスを提供するためです。

これらの変更は、Go言語の bytes パッケージが提供する基本的な操作の効率を大幅に向上させ、Goで開発されるアプリケーションの全体的なパフォーマンスに貢献します。特に、Equal のアセンブリ最適化は、低レベルなメモリ操作のパフォーマンスを追求するGoの哲学を体現しています。

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

このコミットで変更された主要なファイルと関数は以下の通りです。

  • src/pkg/bytes/asm_386.s:
    • TEXT ·Equal(SB): Equal 関数の 32-bit x86 アセンブリ実装が追加されました。
  • src/pkg/bytes/asm_amd64.s:
    • TEXT ·Equal(SB): Equal 関数の 64-bit AMD64 アセンブリ実装が追加されました。
  • src/pkg/bytes/asm_arm.s:
    • TEXT ·Equal(SB): ARM アーキテクチャ向けに equalPortable(Goで書かれた実装)に分岐するスタブが追加されました。
  • src/pkg/bytes/bytes.go:
    • func Equal(a, b []byte) bool: 宣言が変更され、実際のGo実装は equalPortable に移動しました。Equal は外部(アセンブリ)実装を呼び出すようになりました。
    • func equalPortable(a, b []byte) bool: 以前の Equal のGo実装がこの関数にリネームされました。
    • func Count(s, sep []byte) int: アルゴリズムが大幅に書き直され、IndexByte を活用したスキップロジックが導入されました。
    • func Index(s, sep []byte) int: アルゴリズムが大幅に書き直され、IndexByte を活用したスキップロジックと、sep の長さが1の場合の最適化が導入されました。
  • src/pkg/bytes/bytes_test.go:
    • TestCompare: EqualPortable のテストが追加されました。
    • BenchmarkIndexByte*: IndexByte のベンチマークが追加・修正されました。
    • BenchmarkEqual*, BenchmarkEqualPort*: EqualEqualPortable のベンチマークが追加されました。
    • BenchmarkIndex*, BenchmarkIndexEasy*: Index のベンチマークが追加・修正されました。
    • BenchmarkCount*, BenchmarkCountEasy*: Count のベンチマークが追加されました。
    • bmIndexByte, bmEqual, bmIndex, bmIndexEasy, bmCount, bmCountEasy: 新しいベンチマークヘルパー関数が追加されました。
  • src/pkg/bytes/export_test.go:
    • var EqualPortable = equalPortable: equalPortable 関数がテストのためにエクスポートされました。

コアとなるコードの解説

bytes.Equal のアセンブリ実装 (asm_amd64.s の例)

TEXT ·Equal(SB),7,$0
	MOVL	len+8(FP), BX   // len (aの長さ) を BX にロード
	MOVL	len1+24(FP), CX // len1 (bの長さ) を CX にロード
	MOVL	$0, AX          // AX を 0 に初期化 (戻り値 false)
	MOVL	$1, DX          // DX を 1 に初期化 (戻り値 true)
	CMPL	BX, CX          // a と b の長さを比較
	JNE	eqret           // 長さが異なれば eqret へジャンプ (false を返す)
	MOVQ	p+0(FP), SI     // p (aのポインタ) を SI にロード
	MOVQ	q+16(FP), DI    // q (bのポインタ) を DI にロード
	CLD                     // 方向フラグをクリア (SI/DI をインクリメント方向にする)
	REP; CMPSB               // SI と DI が指すバイトを CX 回比較
	CMOVLEQ	DX, AX          // 比較結果が等しければ (ZF=1)、AX に DX (1) を移動 (true を返す)
eqret:
	MOVB	AX, ret+32(FP)  // AX の下位バイトを戻り値に設定
	RET                     // 関数から戻る

このアセンブリコードは、bytes.Equal 関数がどのように高速化されたかを示しています。

  1. まず、2つのバイトスライスの長さが比較されます。長さが異なる場合、即座に false が返されます。
  2. 長さが同じ場合、REP; CMPSB 命令が使用されます。これは、SI レジスタが指すメモリ(スライス a の開始アドレス)と DI レジスタが指すメモリ(スライス b の開始アドレス)を、CX レジスタに格納された長さの分だけバイト単位で比較します。
  3. REP; CMPSB は、比較中に不一致が見つかった場合、またはすべてのバイトが比較された場合に停止します。
  4. 比較が完了した後、CMOVLEQ DX, AX 命令は、比較結果が等しい(Zero Flagがセットされている)場合にのみ AX レジスタに 1true を意味する)を移動させます。それ以外の場合は AX0 のままです。
  5. 最終的に AX の値が関数の戻り値として設定されます。

この REP; CMPSB 命令は、CPUが内部的に最適化されたループでバイト比較を実行するため、GoやCで書かれた同等のループよりもはるかに高速です。

bytes.Count のGo実装 (bytes.go の変更点)

func Count(s, sep []byte) int {
	n := len(sep)
	if n == 0 {
		return utf8.RuneCount(s) + 1
	}
	if n > len(s) {
		return 0
	}
	count := 0
	c := sep[0] // 検索パターンの最初のバイト
	i := 0
	t := s[:len(s)-n+1] // 検索範囲を限定
	for i < len(t) {
		if t[i] != c { // 最初のバイトが一致しない場合
			o := IndexByte(t[i:], c) // IndexByteで次の出現位置を高速検索
			if o < 0 {
				break // 見つからなければ終了
			}
			i += o // 見つかった位置までスキップ
		}
		// 最初のバイトが一致した、またはスキップしてきた場合
		if n == 1 || Equal(s[i:i+n], sep) { // sepの長さが1か、完全一致の場合
			count++
			i += n // sepの長さ分だけ進む
			continue
		}
		i++ // 一致しない場合は1バイト進む
	}
	return count
}

新しい Count 関数は、検索パターン sep の最初のバイト c を利用して、効率的に検索位置をスキップする戦略を採用しています。

  1. sep の長さが0の場合(空のパターンを数える場合)は、UTF-8ルーンの数に1を加えたものを返します。
  2. sep の長さが s より長い場合は、0を返します。
  3. メインのループでは、現在の位置 i のバイトが sep の最初のバイト c と異なる場合、IndexByte(t[i:], c) を呼び出して、c が次に出現する位置まで i を一気に進めます。これにより、多くの不要なバイト比較をスキップできます。
  4. c が見つかった、または現在の位置が c と一致する場合、sep の長さが1であれば即座にカウントし、そうでなければ Equal(s[i:i+n], sep) で完全なパターンマッチングを行います。
  5. マッチした場合、count をインクリメントし、sep の長さ分だけ i を進めて次の検索を開始します。
  6. マッチしなかった場合、i を1バイトだけ進めて次のループを続けます。

この「IndexByte を使ったスキップ」戦略が、「イージーケース」での大幅な速度向上に貢献しています。

bytes.Index のGo実装 (bytes.go の変更点)

func Index(s, sep []byte) int {
	n := len(sep)
	if n == 0 {
		return 0
	}
	if n > len(s) {
		return -1
	}
	c := sep[0]
	if n == 1 { // sepの長さが1の場合の最適化
		return IndexByte(s, c)
	}
	i := 0
	t := s[:len(s)-n+1]
	for i < len(t) {
		if t[i] != c { // 最初のバイトが一致しない場合
			o := IndexByte(t[i:], c) // IndexByteで次の出現位置を高速検索
			if o < 0 {
				break // 見つからなければ終了
			}
			i += o // 見つかった位置までスキップ
		}
		// 最初のバイトが一致した、またはスキップしてきた場合
		if Equal(s[i:i+n], sep) { // 完全一致の場合
			return i // 見つかったインデックスを返す
		}
		i++ // 一致しない場合は1バイト進む
	}
	return -1 // 見つからなかった
}

Index 関数も Count 関数と同様に、IndexByte を活用したスキップ戦略を採用しています。

  1. sep の長さが0の場合、0を返します。
  2. sep の長さが s より長い場合、-1を返します。
  3. 重要な最適化: sep の長さが1の場合、IndexByte(s, c) を直接呼び出します。これは、単一バイトの検索が非常に一般的であり、これに特化した高速パスを提供するためです。
  4. メインのループでは、Count と同様に、sep の最初のバイト cIndexByte で効率的に検索し、その位置までスキップします。
  5. c が見つかった場合、Equal(s[i:i+n], sep) で完全なパターンマッチングを行います。
  6. マッチした場合、現在のインデックス i を返して終了します。
  7. マッチしなかった場合、i を1バイトだけ進めて次のループを続けます。
  8. ループが終了しても見つからなければ、-1を返します。

これらの変更により、bytes パッケージの主要な文字列/バイトスライス操作が、特に一般的なシナリオにおいて大幅に高速化されました。

関連リンク

参考にした情報源リンク

  • Go言語のソースコード (特に src/pkg/bytes ディレクトリ)
  • x86/AMD64 アセンブリ命令セットリファレンス (例: Intel Software Developer's Manuals)
  • Go言語のベンチマークに関するドキュメント
  • bytes.Equal のアセンブリ実装に関する議論 (GoコミュニティのメーリングリストやIssueトラッカー)