[インデックス 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))
この変更により、buffer
はlayout
文字列の長さに等しい初期容量を持つバイトスライスとして作成されます。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))
この変更により、b
はmake
関数を使って明示的に初期化されます。
make([]byte, 0, len(layout))
は、byte
型のスライスを作成します。- 最初の引数
0
は、スライスの初期長さを0に設定します。これは、スライスがまだデータを含んでいないことを意味します。 - 二番目の引数
len(layout)
は、スライスの初期容量をlayout
文字列の長さに設定します。これにより、Format
関数がlayout
文字列の長さに応じたバイト数を追加する際に、ほとんどの場合、追加のメモリ再割り当てなしで処理を進めることができます。
この最適化は、Format
メソッドが頻繁に呼び出されるようなシナリオにおいて、メモリ割り当てとコピーのオーバーヘッドを削減し、全体的なパフォーマンスを向上させる効果があります。
関連リンク
- Go CL 5992058: https://golang.org/cl/5992058
参考にした情報源リンク
- Go言語の公式ドキュメント(
time
パッケージ、スライス、make
関数に関する情報) - Go言語のパフォーマンス最適化に関する一般的な情報(メモリ割り当て、プロファイリングなど)
- コミットメッセージに記載された情報