[インデックス 16363] ファイルの概要
このコミットは、Go言語の標準ライブラリtimeパッケージからTime.FormatAppend関数を削除するものです。この関数は、Time.Formatの非アロケーションバージョンとして導入されましたが、コンセンサスが得られなかったため元に戻されました。
コミット
commit 509a1173a33d4fd914409cd941470d440fd5eed3
Author: Brad Fitzpatrick <bradfitz@golang.org>
Date: Tue May 21 14:32:09 2013 -0700
time: remove Time.FormatAppend
undo CL 8478044 / 0d28fd55e721
Lack of consensus.
««« original CL description
time: add Time.FormatAppend
This is a version of Time.Format that doesn't require allocation.
Fixes #5192
Update #5195
R=r
CC=gobot, golang-dev
https://golang.org/cl/8478044
»»»
R=r
CC=golang-dev
https://golang.org/cl/9462049
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/509a1173a33d4fd914409cd941470d440fd5eed3
元コミット内容
このコミットは、以前のコミット (CL 8478044 / 0d28fd55e721) を元に戻すものです。元のコミットでは、timeパッケージにTime.FormatAppendという新しい関数が追加されました。この関数の目的は、既存のTime.Format関数が文字列を返す際に発生するメモリ割り当て(アロケーション)を避けることでした。FormatAppendは、[]byteスライスに時刻のテキスト表現を追記し、拡張されたバッファを返すことで、アロケーションを不要にすることを目指していました。これは、特にパフォーマンスが重要なアプリケーションにおいて、頻繁な時刻のフォーマット処理によるガベージコレクションの負荷を軽減することを意図していました。
変更の背景
Time.FormatAppendの導入は、パフォーマンス最適化を目的としていましたが、このコミットメッセージには「Lack of consensus.」(コンセンサスが得られなかった)と明記されています。これは、Goコミュニティ内またはGo開発チーム内で、この新しいAPIの設計、必要性、あるいは既存のTime.Formatとの関係性について意見の相違があったことを示唆しています。
一般的に、Go言語の標準ライブラリに新しいAPIを追加する際には、その設計がGoの哲学(シンプルさ、明瞭さ、効率性)に合致しているか、既存のAPIとの一貫性があるか、そして将来的なメンテナンス性など、多角的な観点から厳密なレビューが行われます。Time.FormatAppendは、アロケーションを避けるという明確な利点があったにもかかわらず、何らかの理由でこれらの基準を満たさないと判断されたか、あるいはそのメリットがAPIの複雑性増加や他の潜在的なデメリットを上回らないと判断された可能性があります。
後のGoのバージョン(Go 1.24.0以降)では、time.Time型にAppendTextという類似の機能を持つメソッドが導入されています。これは、Time.FormatAppendが目指した「既存のバイトスライスにテキスト表現を追記する」というコンセプトが、形を変えて再評価され、より適切なAPIとして実装された可能性を示唆しています。
前提知識の解説
Go言語のtimeパッケージ
Go言語のtimeパッケージは、時刻の表現、操作、フォーマット、および時間間隔の測定を行うための機能を提供します。主要な型としてtime.Timeがあり、これは特定の時点を表します。
time.Time.Format関数
time.Time.Format(layout string) stringは、time.Time型の値を指定されたレイアウト文字列に従ってフォーマットし、その結果を新しいstringとして返します。レイアウト文字列は、Go言語独自の参照時刻(Mon Jan 2 15:04:05 MST 2006)に基づいてフォーマットを指定します。
メモリ割り当て(アロケーション)とガベージコレクション
Go言語はガベージコレクタを持つ言語であり、メモリ管理の多くを自動で行います。しかし、新しい文字列やスライスが作成されるたびに、ヒープメモリに新しい領域が割り当てられます(アロケーション)。これらのアロケーションが頻繁に発生すると、ガベージコレクタがより頻繁に実行され、プログラムの実行が一時的に停止する(ストップ・ザ・ワールド)など、パフォーマンスに影響を与える可能性があります。
特に、ループ内でTime.Formatのように新しい文字列を生成する処理が繰り返される場合、大量の短期的なオブジェクトが生成され、ガベージコレクションの負荷が高まることがあります。これを避けるために、既存のバッファにデータを追記するようなAPI(例: bytes.Bufferや、このコミットで削除されたFormatAppendのようなもの)が検討されることがあります。
[]byteとstringの変換
Go言語では、[]byteとstringは異なる型であり、相互に変換する際には通常、新しいメモリ割り当てが発生します。string(b)のように[]byteをstringに変換すると、bの内容が新しい文字列としてコピーされます。パフォーマンスが重要な場面では、この変換コストも考慮に入れる必要があります。
技術的詳細
このコミットの技術的詳細は、主にTime.FormatAppendの削除と、それに伴うTime.Format関数の内部実装の変更に集約されます。
元のTime.FormatAppendは、[]byteスライスを引数として受け取り、そのスライスにフォーマットされた時刻データを追記して、拡張された[]byteスライスを返していました。これにより、呼び出し元は事前に確保したバッファを再利用でき、Time.Formatが毎回新しい文字列を生成する際に発生するアロケーションを回避できました。
このコミットでは、Time.FormatAppend関数自体が削除されます。そして、Time.Format関数が、Time.FormatAppendが内部で行っていたバッファ管理のロジックを直接取り込む形に変更されます。具体的には、Time.Formatの内部で、小さな固定サイズのバッファ([64]byte)をスタック上に確保し、レイアウト文字列の長さに応じて必要であればヒープに大きなバッファを割り当てるという最適化が行われます。最終的に、この内部バッファの内容をstringに変換して返します。
この変更により、Time.Formatは引き続きstringを返すAPIとして一貫性を保ちつつ、内部的にはある程度のパフォーマンス最適化(特に短いフォーマット文字列の場合)を維持します。しかし、呼び出し元が独自の[]byteバッファを再利用してアロケーションを完全に回避する手段は、この時点では提供されなくなります。
コアとなるコードの変更箇所
変更はsrc/pkg/time/format.goファイルに集中しています。
--- a/src/pkg/time/format.go
+++ b/src/pkg/time/format.go
@@ -380,22 +380,6 @@ func (t Time) String() string {
// about the formats and the definition of the reference time, see the
// documentation for ANSIC and the other constants defined by this package.
func (t Time) Format(layout string) string {
-- const bufSize = 64
-- var b []byte
-- max := len(layout) + 10
-- if max <= bufSize {
-- var buf [bufSize]byte
-- b = buf[:0]
-- } else {
-- b = make([]byte, 0, max)
-- }
-- b = t.FormatAppend(layout, b)
-- return string(b)
-}
-
-// FormatAppend works like Format but appends the textual
-// representation to b and returns the extended buffer.
-func (t Time) FormatAppend(layout string, b []byte) []byte {
var (
name, offset, abs = t.locabs()
@@ -405,7 +389,16 @@ func (t Time) FormatAppend(layout string, b []byte) []byte {
hour int = -1
min int
sec int
++
++ b []byte
++ buf [64]byte
)
++ max := len(layout) + 10
++ if max <= len(buf) {
++ b = buf[:0]
++ } else {
++ b = make([]byte, 0, max)
++ }
// Each iteration generates one std value.
for layout != "" {
prefix, std, suffix := nextStdChunk(layout)
@@ -553,7 +546,7 @@ func (t Time) FormatAppend(layout string, b []byte) []byte {
\tb = formatNano(b, uint(t.Nanosecond()), std>>stdArgShift, std&stdMask == stdFracSecond9)
}\n \t}\n-- return b
-+ return string(b)
}\n \n var errBad = errors.New("bad value for field") // placeholder not passed to user
コアとなるコードの解説
このコミットの主要な変更点は以下の通りです。
-
Time.FormatAppend関数の削除:func (t Time) FormatAppend(layout string, b []byte) []byteというシグネチャを持つ関数が完全に削除されました。これにより、外部から[]byteバッファを渡して時刻をフォーマットし、アロケーションを避けるという直接的な手段がなくなりました。 -
Time.Format関数の内部実装の変更: 削除されたTime.FormatAppendが持っていたバッファ管理ロジックが、Time.Format関数に直接統合されました。- 変更前:
Time.Formatは内部でbufSize(64バイト)のスタックバッファを試み、必要に応じてヒープにmake([]byte, 0, max)でバッファを割り当て、そのバッファをt.FormatAppend(layout, b)に渡していました。そして、FormatAppendが返した[]byteをstring(b)で文字列に変換して返していました。 - 変更後:
Time.Format関数は、Time.FormatAppendの呼び出しを削除し、その代わりにTime.FormatAppendの内部ロジックを自身の内部に直接取り込みました。var b []byteとvar buf [64]byteが宣言されます。max := len(layout) + 10で必要な最大バッファサイズを計算します。if max <= len(buf)の条件で、レイアウト文字列の長さが64バイトのスタックバッファに収まる場合はb = buf[:0]としてスタックバッファを使用します。- 収まらない場合は
b = make([]byte, 0, max)としてヒープに新しいバッファを割り当てます。 - 時刻のフォーマット処理は、以前
FormatAppendが行っていたのと同様に、bにデータを追記していきます。 - 最終的に、
return string(b)として、フォーマットされた[]byteバッファの内容を新しいstringに変換して返します。
- 変更前:
この変更により、Time.Formatは引き続きstringを返すAPIとして提供されますが、内部的には短いフォーマット文字列の場合にスタックアロケーションを利用することで、ヒープアロケーションを避ける最適化が施されています。しかし、呼び出し元がバッファを再利用してアロケーションを完全に制御することはできなくなりました。
関連リンク
- 元のコミット (CL 8478044): https://golang.org/cl/8478044
- このコミット (CL 9462049): https://golang.org/cl/9462049
- 関連するIssue:
- Fixes #5192
- Update #5195
参考にした情報源リンク
- Go言語
timeパッケージのドキュメント: https://pkg.go.dev/time - Go言語における
stringと[]byteの変換とアロケーションに関する一般的な情報源 - Go 1.24.0で導入された
Time.AppendTextに関する情報:- go.dev: https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQE_uS7btFIt4MIWP7-1UfkcCpDbsi7jc95BXoepYdx06tpKQZwyiw6L7SEU0TKZTlnAHp1WGDxxEGQAD3Zsz4aPRKmOZqhiqGD_LdtzVP4ZrHrB
- github.com (Goソースコード): https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQHx0Huwm888GIoqDrr8oPhZph5oKgb3OUI-ES5yzQsqhEQOdye949cQiUBDj5eY3-hu0fNG-W4tUduOMkxTuzSakLBYC_Ifsnrwp4tniImpcPce941GL89Rz7UTLOHNFOBsIDQu
- github.com (MarshalTextの内部使用例): https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQGqd3RuUeke2WChlCdUSVR9JXd6pwYqt0tyPXGJWV4Wct9sPlvxDLstnl8jN8lzonrOvaZyt78qkbF6QlHxz-_Gt8x5ifxyazh_BwhKjtLRgSjerqgf9fDfoSTPHym2WbWjStm-muTvkjcsY5QzP2ENujtmNg==