メインコンテンツへスキップ
#81698accepted

maps: add Grow

ステータス変更: likely_accept accepted

要約

AIによる要約であり、誤りを含む場合があります。

概要

既存のマップの容量を、あとから追加する要素数に合わせて前もって広げておく maps.Grow 関数を maps パッケージに追加する提案です。あわせて、maps.Copy もコピー先のマップを内部で事前に拡張するように変更されます。

ステータス変更

likely_accept → accepted
2026年10月14日の Proposal Review Meeting で likely accept となり、その後コンセンサスに変化がなかったため、10月29日に @aclements が accepted を宣言しました。委員会の判断は次の3点です。

  • clear のような組み込み関数にはせず、ライブラリ関数として追加する。Go のコードだけでは書けない「少し魔法的な」関数になるが、強く反対するメンバーはいなかった。
  • 引数 n はあくまでヒントであり、それ以上の拡張が起きないことは保証しない。この点をドキュメントに明記する。
  • maps.Copy も内部で自動的に事前拡張する。
    likely accept の後、将来ランタイムがマップを自動で縮小するようになった場合(#20135)に矛盾しないかという懸念が出ました。これに対し @aclements は「縮小のポリシーはランタイム側で決められる。たとえば Grow を呼んだマップは縮小しない、といった方針で両立できる」と回答し、承認に至りました。

技術的背景

現状の問題点

make(M, hint) を使えば新しいマップを最初から必要な大きさで作れますし、maps.Clone は今のサイズのままマップを複製します。しかし、すでに存在するマップに大量の要素を足すと、要素が増えるたびに段階的なリハッシュ(ハッシュテーブルの作り直しと再配置)が何度も起きます。
特に困るのは、http.ResponseWriter.Header() のように他者が所有するマップへ書き込む場合です。この場合はマップを新しいものに差し替えられないので、make で作り直す方法が使えません。libhoney-go では、事前サイズ指定した新しいマップへ全要素をコピーし直して差し替えるというワークアラウンドを使っていました。これでも本番環境で約5%の CPU 削減になりましたが、呼び出し側がすでに持っているマップへの参照が切れてしまうという副作用がありました。

提案された解決策

// Grow increases m's capacity, if necessary, so that at least n more
// entries can be added without further growth in the typical case, as if
// m had been created with make(M, len(m)+n). Grow never shrinks m.
// The is best effort: it cannot guarantee there will be no additional
// growth.
//
// Grow panics if n is negative. It also panics if m is nil and n > 0,
// as assigning to an element of a nil map would.
func Grow[M ~map[K]V, K comparable, V any](m M, n int)
  • 「あと n 要素」を追加できるように拡張します(Rust の HashMap::reserve と同じ考え方です)。マップを縮小することはありません。
  • マップをその場で更新するため、slices.Grow と違って戻り値はありません。そのため nil マップは扱えず、n > 0 で nil マップを渡すと panic します。
  • maps.Copy(dst, src) は、dst を len(src) まで事前に拡張するようになります。

これによって何ができるようになるか

  • HTTP ヘッダのコピー: httputil.ReverseProxy の copyHeader、net/http/cgi、TimeoutHandler などで、既存のヘッダマップへまとめて書き込むときの再拡張を避けられます。net/http/httputil と net/http/cgi に適用する CL 856621 もすでに出ています。
  • スライスやストリームからの一括挿入: 要素をスライスに集めてから既存のマップに入れる場合に使えます。maps.Copy は元データがマップでないと使えないため、この用途は Grow でしかカバーできません(@jub0bs の CORS ミドルウェアの例)。
  • 共有マップをその場で拡張する: libhoney-go の AddFields のように、マップへの参照を保ったまま事前にサイズを確保できます。

コード例

// Before: 既存マップへの追加では段階的なリハッシュが何度も起きる
func mergeHeaders(w http.ResponseWriter, pairs [][2]string) {
	h := w.Header() // 自分が所有していないので作り直せない
	for _, p := range pairs {
		h.Add(p[0], p[1])
	}
}
// After: 追加する件数分を先に確保してから挿入する
func mergeHeaders(w http.ResponseWriter, pairs [][2]string) {
	h := w.Header()
	maps.Grow(h, len(pairs))
	for _, p := range pairs {
		h.Add(p[0], p[1])
	}
}
// maps.Copy は内部で自動的に事前拡張されるので、呼び出し側の変更は不要
maps.Copy(dst, src)

議論のハイライト

  • 性能効果: @randall77 は「数%の改善のために API を増やしたくない。PGO などで自動化する方が望ましい」と述べつつ、make のサイズヒントと同じ考え方だとして弱い賛成を示しました。これに対し提案者 (@lizthegrey) はベンチマークを示しました。小さいマップへ大きいマップを Copy する場合、AMD 5600X で処理時間が51〜57%短縮し、アロケーションもおよそ半分になります。
  • 言語機能をライブラリでこっそり追加していないか: @magical は、clear が組み込み関数になった経緯を挙げて懸念を示しました。委員会は、clear は複数の型にまたがるためライブラリに置き場所がなかったという事情の違いを考慮し、maps パッケージに置くことを認めました。
  • 内部最適化だけで済むのでは: maps.Copy の内部最適化(CL 839065)だけにする案も出ました。しかし、元データがマップでない場合(スライスや Header.Add を使う場合)には効かないため、公開 API も必要と判断されました。Copy は len(src) までしか拡張しないので、キーが重複していても過剰な確保にはなりません。
  • #52157(マップの cap)との関係: Grow は容量の値を外部に見せず、サイズのヒントを受け取るだけです。このため、却下された #52157 とは矛盾しないと整理されています。
  • 将来のマップ自動縮小(#20135)との関係: ドキュメントは「best effort」としているので、ランタイムが自動で縮小することは禁止していません。どちらも縮小ポリシーの設計次第で両立できるとされました。

関連リンク