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

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

このコミットは、Go言語の標準ライブラリtimeパッケージ内のFormatメソッドにおけるパフォーマンス改善を目的としています。具体的には、日付や時刻のフォーマット処理中に使用される内部バッファの初期容量を事前に確保することで、不要なメモリ再割り当て(reallocation)を削減し、CPU使用率を低下させています。

コミット

commit c9529e02c1454de4e88f402df666cdccec25a744
Author: Bobby Powers <bobbypowers@gmail.com>
Date:   Sat Apr 7 10:51:32 2012 +1000

    time: in Format give buffer an initial capacity
    
    I have a small web server that simply sets several cookies
    along with an expires header, and then returns.  In the
    cpuprofile for a 200k request benchmark, time.Time.Format()
    was showing up as 8.3% of cpu usage.  Giving the buffer an
    inital capacity to avoid reallocs on append drops it down to
    7.6%.
    
    R=golang-dev, r
    CC=golang-dev
    https://golang.org/cl/5992058

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

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

元コミット内容

time: in Format give buffer an initial capacity

I have a small web server that simply sets several cookies
along with an expires header, and then returns.  In the
cpuprofile for a 200k request benchmark, time.Time.Format()
was showing up as 8.3% of cpu usage.  Giving the buffer an
inital capacity to avoid reallocs on append drops it down to
7.6%.

R=golang-dev, r
CC=golang-dev
https://golang.org/cl/5992058

変更の背景

コミットメッセージによると、この変更は、とある小規模なWebサーバーのCPUプロファイル分析から生まれました。このWebサーバーは、複数のクッキーとExpiresヘッダーを設定して応答を返すシンプルなものでした。20万リクエストのベンチマークを実行した際のCPUプロファイルにおいて、time.Time.Format()メソッドがCPU使用率の8.3%を占めていることが判明しました。

この高いCPU使用率の原因は、Formatメソッドが内部で文字列を構築する際に、バッファの容量が不足するたびに発生するメモリの再割り当て(reallocation)にあると推測されました。再割り当ては、既存のデータを新しいより大きなメモリ領域にコピーする操作を伴うため、パフォーマンスに大きなオーバーヘッドをもたらします。

この問題を解決し、CPU使用率を削減するために、Formatメソッド内で使用されるバッファに適切な初期容量を事前に与えることで、再割り当ての頻度を減らすことが提案されました。この変更により、time.Time.Format()のCPU使用率は8.3%から7.6%に低下し、全体のパフォーマンスが改善されました。

前提知識の解説

Go言語におけるtime.Time.Format()メソッド

time.Time.Format()メソッドは、Go言語の標準ライブラリtimeパッケージに属するTime型のメソッドです。このメソッドは、Timeオブジェクトが保持する日付と時刻の情報を、指定されたレイアウト文字列に従って整形し、文字列として返します。

例えば、t.Format("2006-01-02 15:04:05")のように使用され、2024-07-18 10:30:00のような形式の文字列を生成します。レイアウト文字列は、Go言語の特定の参照時刻(Mon Jan 2 15:04:05 MST 2006)の各要素に対応する数字や記号を組み合わせることで、出力フォーマットを定義します。

内部的には、このメソッドは指定されたレイアウトとTimeオブジェクトの値を基に、文字を一つずつバッファに追加していくことで最終的な文字列を構築します。

Go言語におけるスライスとバッファ、make関数

Go言語のスライスは、可変長シーケンスを扱うための強力なデータ構造です。スライスは、内部的に配列を参照しており、その長さ(len)と容量(cap)を持っています。

  • 長さ(Length): スライスに含まれる要素の数。
  • 容量(Capacity): スライスの基盤となる配列が保持できる要素の最大数。

スライスに要素を追加する際(例: append関数を使用)、スライスの長さが容量を超えると、Goランタイムはより大きな新しい基盤配列を割り当て、既存の要素を新しい配列にコピーし、スライスが新しい配列を参照するように更新します。このプロセスが「メモリの再割り当て(reallocation)」です。再割り当ては、特に頻繁に発生する場合、パフォーマンスに大きな影響を与えます。

make関数は、スライス、マップ、チャネルなどの組み込み型を初期化するために使用されます。スライスの場合、make([]Type, length, capacity)の形式で呼び出すことで、初期の長さと容量を指定してスライスを作成できます。

  • make([]byte, 0, len(layout))という記述は、byte型のスライスを作成することを意味します。
    • 0はスライスの初期長さを0に設定することを示します。つまり、最初は要素を含みません。
    • len(layout)はスライスの初期容量をlayout文字列の長さに設定することを示します。これは、最終的にフォーマットされる文字列の長さがlayout文字列の長さに近いと予想される場合に、将来のappend操作による再割り当てを避けるための最適化です。

CPUプロファイリング

CPUプロファイリングは、プログラムの実行中にCPUがどの関数やコードブロックに時間を費やしているかを分析する手法です。これにより、プログラムのパフォーマンスボトルネック(処理が遅い原因となっている部分)を特定できます。プロファイリングツールは、関数の呼び出し回数、実行時間、メモリ使用量などの統計情報を収集し、開発者が最適化すべき領域を特定するのに役立ちます。

このコミットの背景では、CPUプロファイリングによってtime.Time.Format()がボトルネックであることが特定され、その後の最適化につながっています。

技術的詳細

このコミットの技術的な核心は、Go言語のスライスにおけるメモリ再割り当てのオーバーヘッドを削減することにあります。

time.Time.Format()関数は、内部でbufferという名前のバイトスライスを使用して、整形された時刻文字列を構築します。変更前は、このbufferは単にbuffer型として宣言されており、初期容量が指定されていませんでした。Goのスライスは、初期容量が指定されない場合、通常は非常に小さいデフォルト容量(またはゼロ容量)で開始されます。

時刻のフォーマット処理では、レイアウト文字列の長さに応じて、最終的な出力文字列の長さが事前に予測できます。例えば、"2006-01-02 15:04:05"というレイアウトであれば、出力文字列は固定長になります。しかし、初期容量が小さいと、Format関数が文字をバッファに追加していく過程で、バッファの容量が頻繁に不足し、そのたびに新しいより大きなメモリ領域が割り当てられ、既存のデータがコピーされる「再割り当て」が発生します。この再割り当ては、特に大量の時刻フォーマット処理が行われるようなシナリオ(例: Webサーバーが多数のHTTPヘッダーを生成する際)では、CPU時間を大きく消費する原因となります。

このコミットでは、bufferの宣言を以下のように変更しました。

-		b     buffer
+		b     buffer = make([]byte, 0, len(layout))

この変更により、bufferlayout文字列の長さに等しい初期容量を持つバイトスライスとして作成されます。len(layout)は、フォーマットされる文字列の最大長を合理的に見積もるための良いヒューリスティックです。これにより、ほとんどの場合、Format関数が文字列を構築する過程で、バッファの容量が不足して再割り当てが発生するのを防ぐことができます。

結果として、メモリのコピー操作が大幅に削減され、CPU使用率が低下し、time.Time.Format()の実行効率が向上しました。コミットメッセージにあるように、CPU使用率が8.3%から7.6%に減少したことは、この小さな変更がもたらした顕著なパフォーマンス改善を示しています。これは、Go言語における効率的なメモリ管理と、プロファイリングに基づく最適化の重要性を示す良い例です。

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

変更はsrc/pkg/time/format.goファイルの一箇所のみです。

--- a/src/pkg/time/format.go
+++ b/src/pkg/time/format.go
@@ -367,7 +367,7 @@ func (t Time) Format(layout string) string {
 	\thour  int = -1
 	\tmin   int
 	\tsec   int
-\t\tb     buffer
+\t\tb     buffer = make([]byte, 0, len(layout))
 	)
 	// Each iteration generates one std value.
 	for {

コアとなるコードの解説

変更された行は、Formatメソッド内で使用されるbuffer変数の初期化部分です。

変更前:

		b     buffer

これは、bという名前のbuffer型の変数を宣言していますが、その初期値や容量はGoのデフォルトのゼロ値(スライスの場合、nilスライスまたは長さ0、容量0のスライス)に依存していました。これにより、Format関数が文字列を構築するためにbにバイトを追加していく際に、頻繁に内部配列の再割り当てが発生する可能性がありました。

変更後:

		b     buffer = make([]byte, 0, len(layout))

この変更により、bmake関数を使って明示的に初期化されます。

  • make([]byte, 0, len(layout))は、byte型のスライスを作成します。
  • 最初の引数0は、スライスの初期長さを0に設定します。これは、スライスがまだデータを含んでいないことを意味します。
  • 二番目の引数len(layout)は、スライスの初期容量をlayout文字列の長さに設定します。これにより、Format関数がlayout文字列の長さに応じたバイト数を追加する際に、ほとんどの場合、追加のメモリ再割り当てなしで処理を進めることができます。

この最適化は、Formatメソッドが頻繁に呼び出されるようなシナリオにおいて、メモリ割り当てとコピーのオーバーヘッドを削減し、全体的なパフォーマンスを向上させる効果があります。

関連リンク

参考にした情報源リンク

  • Go言語の公式ドキュメント(timeパッケージ、スライス、make関数に関する情報)
  • Go言語のパフォーマンス最適化に関する一般的な情報(メモリ割り当て、プロファイリングなど)
  • コミットメッセージに記載された情報