📈

Go1.23でのスライスのキャパシティ増加を深掘り:appendの動作を探る

に公開

実験

実験の目的

Goのappend関数は、スライスの容量が不足すると自動的に新しいメモリ領域を確保して再割り当てを行います。このとき、スライスのキャパシティ(cap)がどのように変化するか、規則性を調べるための実験を行います。

実験コード

以下のコードでは、スライスに要素を追加していき、その都度スライスの長さとキャパシティ、キャパシティの増加率を確認しています。

package main

import "fmt"

func main() {
	// 初期キャパシティを持たないスライスを定義
	var numbers []float64
	lastCap := cap(numbers)

	// counts 回 append し、キャパシティの変動を確認
	counts := 10000
	fmt.Printf("len, cap, growth\n")
	for i := range counts {
		numbers = append(numbers, float64(i))
		// キャパシティが変化したときのみ出力
		if cap(numbers) != lastCap {
			fmt.Printf("%d, %d, %f\n", len(numbers), cap(numbers), calGrowth(lastCap, cap(numbers)))
			lastCap = cap(numbers)
		}
	}
}

// calGrowth はキャパシティの増加率を計算する
func calGrowth(lastCap, cap int) float32 {
	if lastCap == 0 {
		return 0
	}
	return float32(cap) / float32(lastCap)
}

実験結果

コード出力
$ go run ./main.go 
len, cap, growth
1, 1, 0.000000
2, 2, 2.000000
3, 4, 2.000000
5, 8, 2.000000
9, 16, 2.000000
17, 32, 2.000000
33, 64, 2.000000
65, 128, 2.000000
129, 256, 2.000000
257, 512, 2.000000
513, 848, 1.656250
849, 1280, 1.509434
1281, 1792, 1.400000
1793, 2560, 1.428571
2561, 3408, 1.331250
3409, 5120, 1.502347
5121, 7168, 1.400000
7169, 9216, 1.285714
9217, 12288, 1.333333

実験を通じて、スライスのキャパシティが一定の条件で増加している様子を観察しました。スライスのキャパシティが小さいうちは2倍の増加が続きますが、キャパシティが1024を超えると、増加率が2倍から1.25倍〜1.5倍程度に減少しています。

キャパシティの増加量は一定ではないようです。この理由を探すと下記のブログ記事に行きつきました。

ブログ記事「Sliceのcapacityはどのように増加していくか」の内容を踏まえた考察

Goの内部実装の理解

ブログ記事「Sliceのcapacityはどのように増加していくか」によると、キャパシティの増加は以下のような段階で行われています。

  1. 仮の新しいキャパシティを決定する
    • キャパシティが1024未満の場合は2倍に。
    • 1024以上の場合は1.25倍に増加。
  2. 実際のキャパシティをメモリアロケータの単位に基づいて決定
    • メモリアロケータの仕様に基づき、確保するメモリ容量がクラスごとに最適化されます。
  3. 新しい基底配列を確保し、要素をコピー
    • 新しい基底配列にコピーし、キャパシティが増加されたスライスを返します。

この一連の処理によって、スライスのキャパシティは効率的かつ柔軟に増加していることがわかります。

メモリアロケータによる最適化

Goは内部でTCMallocというメモリアロケータをベースとした独自のメモリアロケータを使用しており、これにより確保するメモリ容量は自動的に最適化されています。これが、キャパシティの増加率が必ずしも一定ではない理由の一つです。

まとめ

主なポイント

  • キャパシティの増加パターン
    小さなスライスの場合、キャパシティは2倍で増加しますが、大きくなると1.25倍〜1.5倍の範囲で増加します。

  • メモリ効率を考慮した最適化
    Goの内部実装では、メモリアロケータによってメモリ効率が最適化されており、確保するメモリ容量に基づいてキャパシティが調整されます。

実装の理解を深める

スライスのキャパシティの増加は、Goの内部で非常に効率的に制御されています。これにより、スライスを使うときにパフォーマンスを意識しながらコードを書くことが重要です。特に、必要なキャパシティが見積もれる場合は、最初に適切なキャパシティを設定しておくことで、パフォーマンスを改善できます。

Discussion