コンテンツにスキップ

Go/配列とスライスとエスケープ解析

出典: フリー教科書『ウィキブックス(Wikibooks)』
< Go

Go言語における配列とスライスの性能特性を理解するには、エスケープ解析(escape analysis)の仕組みを知ることが重要です。エスケープ解析は、変数がスタックとヒープのどちらに配置されるかを決定するコンパイラの最適化技術です。

エスケープ解析とは

[編集]

エスケープ解析は、変数の生存期間とアクセスパターンを解析し、その変数が関数のスコープを「エスケープ」(脱出)するかを判断します。

エスケープする条件

[編集]
  • 関数の戻り値として返される
  • 他の関数に渡される
  • グローバル変数に代入される
  • goroutineで使用される

配列のエスケープ解析

[編集]

スタックに配置される場合

[編集]
func processArray() {
    var arr [1000]int  // スタックに配置される
    arr[0] = 42
    fmt.Println(arr[0])
    // 関数終了時に自動的にメモリが解放される
}

この場合、配列は関数内でのみ使用され、外部に渡されないためスタックに配置されます。

ヒープに配置される場合

[編集]
func createArray() *[1000]int {
    var arr [1000]int  // ヒープに配置される
    arr[0] = 42
    return &arr  // アドレスを返すため、エスケープする
}

func main() {
    ptr := createArray()
    fmt.Println((*ptr)[0])
    // ガベージコレクションが必要
}

配列のアドレスが返されるため、エスケープ解析によりヒープに配置されます。

スライスのエスケープ解析

[編集]

スライスは常にヒープではない

[編集]
func processSlice() {
    slice := make([]int, 10)  // 小さなスライスはスタックに配置される可能性
    slice[0] = 42
    fmt.Println(slice[0])
    // 関数内でのみ使用される場合
}

エスケープするスライス

[編集]
func createSlice() []int {
    slice := make([]int, 10)  // ヒープに配置される
    slice[0] = 42
    return slice  // スライスを返すため、エスケープする
}

func appendSlice(s []int) []int {
    return append(s, 100)  // appendの結果によってはヒープに再配置
}

実践的な例:性能への影響

[編集]

配列を使った最適化

[編集]
// 高速:スタックに配置される
func sumArray(data [1000]int) int {
    sum := 0
    for _, v := range data {
        sum += v
    }
    return sum
}

// 低速:ヒープアクセスが必要
func sumSlice(data []int) int {
    sum := 0
    for _, v := range data {
        sum += v
    }
    return sum
}

エスケープ解析の確認方法

[編集]
# エスケープ解析の結果を確認
go build -gcflags="-m" main.go
出力例:
./main.go:5:6: moved to heap: arr
./main.go:8:13: &arr escapes to heap

性能に関する考慮事項

[編集]

配列の特徴

[編集]
  • メモリ効率: 事前に確保されたメモリを使用
  • キャッシュ効率: 連続したメモリ配置
  • スタック配置: 小さな配列は高速なスタックアクセス

スライスの特徴

[編集]
  • 柔軟性: 動的サイズ変更
  • メモリオーバーヘッド: ヘッダー情報(長さ、容量、ポインタ)
  • ヒープ配置: 大きなスライスや長期間使用されるスライス

最適化のガイドライン

[編集]

配列を選ぶべき場合

[編集]
  • サイズが固定で事前に分かっている
  • 高頻度でアクセスされる小さなデータ
  • 一時的な処理用バッファ
func processBuffer() {
    var buffer [4096]byte  // 一時的なバッファ
    // 高速な処理
}

スライスを選ぶべき場合

[編集]
  • 動的なサイズ変更が必要
  • 関数間でのデータ共有
  • 大きなデータセット
func processData(data []int) []int {
    result := make([]int, 0, len(data))
    // 柔軟な処理
    return result
}

実測によるベンチマーク例

[編集]
func BenchmarkArray(b *testing.B) {
    for i := 0; i < b.N; i++ {
        var arr [1000]int
        for j := 0; j < 1000; j++ {
            arr[j] = j
        }
    }
}

func BenchmarkSlice(b *testing.B) {
    for i := 0; i < b.N; i++ {
        slice := make([]int, 1000)
        for j := 0; j < 1000; j++ {
            slice[j] = j
        }
    }
}

まとめ

[編集]

エスケープ解析を理解することで、配列とスライスの適切な使い分けが可能になります。パフォーマンスが重要な場面では、エスケープ解析の結果を確認し、必要に応じて配列を選択することで大幅な性能向上が期待できます。ただし、多くの場合、保守性と柔軟性を重視してスライスを選択することが実用的です。