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

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

このコミットは、Go言語の標準ライブラリであるbytesパッケージとstringsパッケージ内のRepeat関数において、バイトスライスや文字列の繰り返し処理を最適化するものです。具体的には、これまで手動のループで行っていた要素のコピー処理を、Go言語組み込みのcopy関数を使用するように変更しています。これにより、パフォーマンスの向上とコードの簡潔化が図られています。

コミット

commit f033d988b1926215f59aa1eee37c05e59adbac02
Author: Evan Shaw <chickencha@gmail.com>
Date:   Tue Aug 27 09:21:08 2013 +1000

    bytes, strings: use copy in Repeat
    
    R=golang-dev, dave, bradfitz, adg
    CC=golang-dev
    https://golang.org/cl/13249043

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

https://github.com/golang/go/commit/f033d988b1926215f59aa1eee37c05e59adbac02

元コミット内容

bytes, strings: use copy in Repeat

R=golang-dev, dave, bradfitz, adg
CC=golang-dev
https://golang.org/cl/13249043

変更の背景

この変更の主な背景は、Go言語の標準ライブラリにおけるパフォーマンスの最適化と、よりGoらしい(idiomatic Go)コードへの移行です。

bytes.Repeat関数とstrings.Repeat関数は、それぞれバイトスライスや文字列を指定された回数だけ繰り返して新しいスライス/文字列を生成する機能を提供します。この処理は、内部で元の要素を新しいメモリ領域にコピーする操作を伴います。

変更前は、このコピー処理がネストされたforループを使って、要素を1つずつ手動でコピーしていました。このような手動でのコピーは、特に大きなスライスや文字列を扱う場合に、以下のようなオーバーヘッドを発生させる可能性がありました。

  1. 関数呼び出しのオーバーヘッド: ループの各イテレーションで、配列のインデックスアクセスや代入といった低レベルな操作が行われます。
  2. 境界チェックのオーバーヘッド: Goのランタイムは、メモリ安全性を保証するために、スライスや配列へのアクセス時に境界チェック(bounds check)を行います。手動ループの場合、各要素へのアクセスごとにこのチェックが発生し、オーバーヘッドが増大します。
  3. 最適化の機会の損失: コンパイラは、手動で書かれたループよりも、組み込みのcopy関数のようなプリミティブな操作に対して、より高度な最適化(例: memmovememcpyのようなアセンブリレベルの命令への変換)を適用しやすい傾向があります。

copy関数はGo言語の組み込み関数であり、Goランタイムによって非常に効率的に実装されています。多くの場合、基盤となるオペレーティングシステムやハードウェアの最適化されたメモリコピー命令(例: x86-64アーキテクチャにおけるrep movsb命令など)を利用するようにコンパイラが最適化するため、手動ループよりもはるかに高速に動作します。

このコミットは、これらのRepeat関数が頻繁に使用される可能性があることを考慮し、より効率的なcopy関数を利用することで、全体的なパフォーマンスを向上させることを目的としています。また、手動ループをcopy関数に置き換えることで、コードがより簡潔になり、意図が明確になるという副次的なメリットもあります。

前提知識の解説

Go言語のスライス (Slice)

Go言語のスライスは、配列のセグメントを参照するデータ構造です。スライスは、長さ(len)と容量(cap)を持ちます。長さはスライスに含まれる要素の数を示し、容量はスライスの開始位置から基盤となる配列の終わりまでの要素の最大数を示します。スライスは動的なサイズ変更が可能であり、Go言語で最も頻繁に使用されるデータ構造の一つです。

Go言語の文字列 (String)

Go言語の文字列は、バイトの読み取り専用スライス([]byte)として内部的に表現されます。Goの文字列は不変(immutable)であり、一度作成されると内容を変更することはできません。文字列操作を行う関数は、通常、新しい文字列を生成して返します。

make関数

make関数は、スライス、マップ、チャネルといった組み込みのデータ構造を初期化するために使用されます。このコミットでは、Repeat関数内で結果を格納するための新しいバイトスライスを事前に確保するためにmake([]byte, len(b)*count)のように使用されています。これにより、必要なメモリが一度に割り当てられ、その後のコピー操作が効率的に行われます。

copy組み込み関数

copyはGo言語の組み込み関数の一つで、ソーススライスからデスティネーションスライスへ要素をコピーします。そのシグネチャはcopy(dst, src []Type) intです。copy関数は、コピーされた要素の数を返します。

copy関数は、Goランタイムによって高度に最適化されています。これは、多くの場合、アセンブリ言語レベルで実装されており、CPUの特定の命令セット(例: SIMD命令やmemmoveのようなブロック転送命令)を利用して、大量のデータを非常に高速にコピーできます。手動でループを回して要素を一つずつコピーするよりも、はるかに効率的です。

bytes.Repeat関数とstrings.Repeat関数

  • bytes.Repeat(b []byte, count int) []byte: 指定されたバイトスライスbcount回繰り返して連結した新しいバイトスライスを返します。
  • strings.Repeat(s string, count int) string: 指定された文字列scount回繰り返して連結した新しい文字列を返します。内部的には、文字列をバイトスライスに変換して処理し、最終的にバイトスライスを文字列に戻します。

これらの関数は、ログメッセージの整形、パターン生成、テストデータの準備など、様々な場面で利用されます。

技術的詳細

このコミットの技術的な核心は、Go言語のcopy組み込み関数が提供するパフォーマンス上の利点を活用することにあります。

変更前は、Repeat関数内で以下のような手動ループが使用されていました。

// bytes.Repeat (変更前)
for i := 0; i < count; i++ {
    for j := 0; j < len(b); j++ {
        nb[bp] = b[j] // 1バイトずつコピー
        bp++
    }
}

// strings.Repeat (変更前)
for i := 0; i < count; i++ {
    for j := 0; j < len(s); j++ {
        b[bp] = s[j] // 1バイトずつコピー
        bp++
    }
}

この手動ループでは、外側のループがcount回、内側のループがlen(b)(またはlen(s))回実行されます。内側のループの各イテレーションで、以下の操作が行われます。

  1. ソーススライスb(またはs)からの要素の読み取り (b[j])
  2. デスティネーションスライスnb(またはb)への要素の書き込み (nb[bp] = ...)
  3. インデックス変数bpjのインクリメント
  4. ループ条件の評価

これらの操作のそれぞれにおいて、Goランタイムはメモリ安全性を保証するために境界チェックを実行します。例えば、nb[bp]への書き込みではbpnbの範囲内にあるか、b[j]の読み取りではjbの範囲内にあるかを確認します。これらのチェックは、ループの各イテレーションで繰り返し行われるため、特にlen(b)countが大きい場合に無視できないオーバーヘッドとなります。

一方、copy関数を使用するように変更された後のコードは以下のようになります。

// bytes.Repeat (変更後)
for i := 0; i < count; i++ {
    bp += copy(nb[bp:], b) // スライス全体を一度にコピー
}

// strings.Repeat (変更後)
for i := 0; i < count; i++ {
    bp += copy(b[bp:], s) // スライス全体を一度にコピー
}

この変更により、内側のループが完全に削除され、代わりにcopy関数が呼び出されます。copy(nb[bp:], b)は、ソーススライスbの全要素を、デスティネーションスライスnbbp以降の領域にコピーします。

copy関数が効率的である理由は以下の通りです。

  1. 単一の境界チェック: copy関数は、コピー操作を開始する前に、ソースとデスティネーションのスライスが有効な範囲内にあるかどうかの境界チェックを一度だけ行います。手動ループのように各要素アクセスごとに行う必要がありません。
  2. コンパイラ最適化: Goコンパイラはcopy関数を特別扱いし、多くの場合、基盤となるCPUアーキテクチャの最適化されたメモリコピー命令(例: memmovememcpy)に直接変換します。これらの命令は、CPUの内部キャッシュやパイプラインを最大限に活用し、非常に高速なデータ転送を実現します。例えば、x86-64アーキテクチャでは、rep movsbのような命令が使用されることがあります。
  3. キャッシュ効率: copy関数は、連続したメモリブロックを効率的に処理するため、CPUのデータキャッシュをより有効に利用できます。これにより、メモリへのアクセスが高速化されます。
  4. Goランタイムのオーバーヘッド削減: 手動ループに比べて、Goランタイムとのインタラクション(例: ガベージコレクションのトリガー、スケジューラのコンテキストスイッチ)が少なくなるため、全体的なオーバーヘッドが削減されます。

この最適化は、特にRepeat関数が生成する結果の長さが非常に大きい場合に顕著なパフォーマンス改善をもたらします。これは、Go言語の標準ライブラリが、ユーザーが意識することなく高いパフォーマンスを提供するための継続的な努力の一環です。

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

src/pkg/bytes/bytes.go

--- a/src/pkg/bytes/bytes.go
+++ b/src/pkg/bytes/bytes.go
@@ -375,10 +375,7 @@ func Repeat(b []byte, count int) []byte {
 	nb := make([]byte, len(b)*count)
 	bp := 0
 	for i := 0; i < count; i++ {
-		for j := 0; j < len(b); j++ {
-			nb[bp] = b[j]
-			bp++
-		}
+		bp += copy(nb[bp:], b)
 	}
 	return nb
 }

src/pkg/strings/strings.go

--- a/src/pkg/strings/strings.go
+++ b/src/pkg/strings/strings.go
@@ -425,10 +425,7 @@ func Repeat(s string, count int) string {
 	b := make([]byte, len(s)*count)
 	bp := 0
 	for i := 0; i < count; i++ {
-		for j := 0; j < len(s); j++ {
-			b[bp] = s[j]
-			bp++
-		}
+		bp += copy(b[bp:], s)
 	}
 	return string(b)
 }

コアとなるコードの解説

src/pkg/bytes/bytes.go の変更

bytes.Repeat関数は、バイトスライスbcount回繰り返して新しいバイトスライスを生成します。

  • 変更前:

    for i := 0; i < count; i++ {
        for j := 0; j < len(b); j++ {
            nb[bp] = b[j]
            bp++
        }
    }
    

    このコードは、外側のループでcount回繰り返し、内側のループで元のバイトスライスbの各バイトをnbに1バイトずつコピーしていました。bpはコピー先のインデックスを追跡しています。

  • 変更後:

    for i := 0; i < count; i++ {
        bp += copy(nb[bp:], b)
    }
    

    内側のfor j := ...ループが削除され、代わりにcopy関数が使用されています。

    • nb[bp:]は、新しく作成されたバイトスライスnbの、現在のbpインデックスから最後までを指すスライスです。これがcopy関数のデスティネーション(コピー先)となります。
    • bは、元のバイトスライスであり、copy関数のソース(コピー元)となります。
    • copy(nb[bp:], b)は、bの全内容をnb[bp:]にコピーします。copy関数は実際にコピーされたバイト数を返します。
    • bp += ...は、copy関数が返したコピーされたバイト数をbpに加算し、次の繰り返しでコピーを開始する位置を更新します。これにより、nbの正しい位置に次の繰り返し部分が書き込まれます。

この変更により、1バイトずつの手動コピーとそれに伴うオーバーヘッドが、最適化されたcopy関数によるブロックコピーに置き換えられ、パフォーマンスが向上します。

src/pkg/strings/strings.go の変更

strings.Repeat関数は、文字列scount回繰り返して新しい文字列を生成します。Goの文字列は不変であるため、内部的にはバイトスライスに変換して処理されます。

  • 変更前:

    for i := 0; i < count; i++ {
        for j := 0; j < len(s); j++ {
            b[bp] = s[j]
            bp++
        }
    }
    

    このコードは、bytes.Repeatと同様に、外側のループでcount回繰り返し、内側のループで元の文字列sの各バイト(文字列はバイトのシーケンスとして扱われる)をbに1バイトずつコピーしていました。

  • 変更後:

    for i := 0; i < count; i++ {
        bp += copy(b[bp:], s)
    }
    

    ここでも、内側の手動ループがcopy関数に置き換えられています。

    • b[bp:]は、新しく作成されたバイトスライスbの、現在のbpインデックスから最後までを指すスライスです。
    • sは、元の文字列です。copy関数は文字列をバイトスライスとして扱うことができます。
    • copy(b[bp:], s)は、文字列sの全内容をb[bp:]にコピーし、コピーされたバイト数を返します。
    • bp += ...は、bpを更新し、次の繰り返しでコピーを開始する位置を調整します。

最終的に、return string(b)で、構築されたバイトスライスbが新しい文字列に変換されて返されます。この変更も、bytes.Repeatと同様に、文字列の繰り返し処理のパフォーマンスを向上させます。

関連リンク

参考にした情報源リンク

  • Go CL 13249043 (このコミットのChange-ID): https://go.dev/cl/13249043
  • Go言語のcopy関数の最適化に関する議論やドキュメント(一般的な情報源として):
    • Goのソースコード(runtimeパッケージ内のmemmoveなどの実装)
    • Goのコンパイラ最適化に関するドキュメントやブログ記事 (例: "Go's work-stealing scheduler", "Go's runtime and garbage collection" など、直接このコミットに言及していなくても、copyの効率性に関する背景情報を提供しているもの)
    • Stack OverflowやGoコミュニティの議論で、copyと手動ループのパフォーマンス比較について言及されているもの。
      • 例: "Why is copy faster than a loop in Go?" (具体的なURLは検索結果による)
  • Go言語の境界チェックに関する情報:
    • Goのコンパイラがどのように境界チェックを最適化するかに関する技術ブログや論文。