Go: ポインタとメモリ管理

Patricia Arquette
リリース: 2024-11-22 01:51:14
オリジナル
438 人が閲覧しました

Go: Pointers & Memory Management

TL;DR: ポインターを使用した Go のメモリ処理、スタックとヒープの割り当て、エスケープ分析、ガベージ コレクションを例を使って説明します

私が初めて Go を学び始めたとき、特にポインターに関するメモリ管理へのアプローチに興味をそそられました。 Go は効率的かつ安全な方法でメモリを処理しますが、内部を覗いてみないと、ちょっとしたブラック ボックスになる可能性があります。 Go がポインター、スタックとヒープ、エスケープ分析やガベージ コレクションなどの概念を使用してメモリをどのように管理するかについて、いくつかの洞察を共有したいと思います。その過程で、これらのアイデアを実際に説明するコード例を見ていきます。

スタックメモリとヒープメモリについて

Go のポインターについて詳しく説明する前に、スタックとヒープがどのように機能するかを理解しておくと役立ちます。これらは変数を保存できる 2 つのメモリ領域であり、それぞれに独自の特性があります。

  • スタック: これは、後入れ先出し方式で動作するメモリ領域です。これは高速かつ効率的で、関数内のローカル変数など、有効期間が短い変数を格納するために使用されます。
  • ヒープ: これは、関数から返されて別の場所で使用されるデータなど、関数のスコープを超えて存在する必要がある変数に使用される、より大きなメモリ プールです。

Go では、変数の使用方法に基づいて、コンパイラーが変数をスタックに割り当てるかヒープに割り当てるかを決定します。この意思決定プロセスは回避分析と呼ばれます。これについては後ほど詳しく説明します。

値渡し: デフォルトの動作

Go では、整数、文字列、ブール値などの変数を関数に渡すとき、それらは自然に値によって渡されます。これは、変数のコピーが作成され、関数がそのコピーを使用して動作することを意味します。つまり、関数内の変数に加えられた変更は、スコープ外の変数には影響しません。

これは簡単な例です:

package main

import "fmt"

func increment(num int) {
    num++
    fmt.Printf("Inside increment(): num = %d, address = %p \n", num, &num)
}

func main() {
    n := 21
    fmt.Printf("Before increment(): n = %d, address = %p \n", n, &n)
    increment(n)
    fmt.Printf("After increment(): n = %d, address = %p \n", n, &n)
}
ログイン後にコピー
ログイン後にコピー
ログイン後にコピー
ログイン後にコピー

出力:

Before increment(): n = 21, address = 0xc000012070 
Inside increment(): num = 22, address = 0xc000012078 
After increment(): n = 21, address = 0xc000012070
ログイン後にコピー
ログイン後にコピー
ログイン後にコピー
ログイン後にコピー

このコード内:

  • increment() 関数は n のコピーを受け取ります。
  • main() の n と increment() の num のアドレスは異なります。
  • increment() 内の num を変更しても、main() の n には影響しません。

要点: 値による受け渡しは安全かつ簡単ですが、大規模なデータ構造の場合、コピーが非効率になる可能性があります。

ポインターの紹介: 参照渡し

関数内の元の変数を変更するには、その変数へのポインタを渡すことができます。ポインタは変数のメモリ アドレスを保持し、関数が元のデータにアクセスして変更できるようにします。

ポインターの使用方法は次のとおりです:

package main

import "fmt"

func incrementPointer(num *int) {
    (*num)++
    fmt.Printf("Inside incrementPointer(): num = %d, address = %p \n", *num, num)
}

func main() {
    n := 42
    fmt.Printf("Before incrementPointer(): n = %d, address = %p \n", n, &n)
    incrementPointer(&n)
    fmt.Printf("After incrementPointer(): n = %d, address = %p \n", n, &n)
}

ログイン後にコピー
ログイン後にコピー
ログイン後にコピー
ログイン後にコピー

出力:

Before incrementPointer(): n = 42, address = 0xc00009a040 
Inside incrementPointer(): num = 43, address = 0xc00009a040 
After incrementPointer(): n = 43, address = 0xc00009a040 
ログイン後にコピー
ログイン後にコピー

この例では:

  • n のアドレスを incrementPointer() に渡します。
  • main() と incrementPointer() はどちらも同じメモリ アドレスを参照します。
  • incrementPointer() 内の num を変更すると、main() の n に影響します。

要点: ポインターを使用すると関数で元の変数を変更できますが、メモリ割り当てに関する考慮事項が必要になります。

ポインタを使用したメモリ割り当て

変数へのポインターを作成するとき、Go はポインターが存在する限り変数が存続することを保証する必要があります。これは多くの場合、変数を スタック ではなく ヒープ に割り当てることを意味します。

次の関数について考えてみましょう:

package main

import "fmt"

func increment(num int) {
    num++
    fmt.Printf("Inside increment(): num = %d, address = %p \n", num, &num)
}

func main() {
    n := 21
    fmt.Printf("Before increment(): n = %d, address = %p \n", n, &n)
    increment(n)
    fmt.Printf("After increment(): n = %d, address = %p \n", n, &n)
}
ログイン後にコピー
ログイン後にコピー
ログイン後にコピー
ログイン後にコピー

ここで、num は createPointer() 内のローカル変数です。 num がスタックに格納されていた場合、関数が返されるとクリーンアップされ、ダングリング ポインタが残ります。これを防ぐために、Go は num をヒープに割り当て、createPointer() が終了した後も有効なままとなるようにします。

ぶら下がりポインター

ダングリング ポインタは、ポインタがすでに解放されたメモリを参照する場合に発生します。

Go はガベージ コレクターによってダングリング ポインターを防止し、メモリが参照されている間にメモリが解放されないようにします。ただし、必要以上にポインターを保持すると、特定のシナリオでメモリ使用量の増加やメモリ リークが発生する可能性があります。

エスケープ分析: スタック割り当てとヒープ割り当ての決定

エスケープ分析は、変数が関数のスコープを超えて存続する必要があるかどうかを判断します。変数が返された場合、ポインターに格納された場合、または goroutine によってキャプチャされた場合、その変数はエスケープされ、ヒープに割り当てられます。ただし、変数がエスケープされない場合でも、コンパイラは、最適化の決定やスタック サイズ制限などの他の理由で変数をヒープに割り当てる場合があります。

変数のエスケープの例:

Before increment(): n = 21, address = 0xc000012070 
Inside increment(): num = 22, address = 0xc000012078 
After increment(): n = 21, address = 0xc000012070
ログイン後にコピー
ログイン後にコピー
ログイン後にコピー
ログイン後にコピー

このコード内:

  • createSlice() のスライス データは main() で返されて使用されるためエスケープされます。
  • スライスの基礎となる配列は、ヒープに割り当てられます。

go build -gcflags '-m' によるエスケープ分析を理解する

-gcflags '-m' オプションを使用すると、Go のコンパイラが何を決定するかを確認できます。

package main

import "fmt"

func incrementPointer(num *int) {
    (*num)++
    fmt.Printf("Inside incrementPointer(): num = %d, address = %p \n", *num, num)
}

func main() {
    n := 42
    fmt.Printf("Before incrementPointer(): n = %d, address = %p \n", n, &n)
    incrementPointer(&n)
    fmt.Printf("After incrementPointer(): n = %d, address = %p \n", n, &n)
}

ログイン後にコピー
ログイン後にコピー
ログイン後にコピー
ログイン後にコピー

これにより、変数がヒープにエスケープされるかどうかを示すメッセージが出力されます。

Go のガベージ コレクション

Go はガベージ コレクターを使用して、ヒープ上のメモリの割り当てと割り当て解除を管理します。参照されなくなったメモリは自動的に解放され、メモリ リークの防止に役立ちます。

例:

Before incrementPointer(): n = 42, address = 0xc00009a040 
Inside incrementPointer(): num = 43, address = 0xc00009a040 
After incrementPointer(): n = 43, address = 0xc00009a040 
ログイン後にコピー
ログイン後にコピー

このコード内:

  • 1,000,000 ノードのリンク リストを作成します。
  • 各ノードは createLinkedList() のスコープをエスケープするため、ヒープ上に割り当てられます。
  • リストが不要になった場合、ガベージ コレクターはメモリを解放します。

要点: Go のガベージ コレクターはメモリ管理を簡素化しますが、オーバーヘッドが発生する可能性があります。

ポインタに関する潜在的な落とし穴

ポインタは強力ですが、慎重に使用しないと問題が発生する可能性があります。

ぶら下がりポインター (続き)

Go のガベージ コレクターはダングリング ポインターの防止に役立ちますが、ポインターを必要以上に長く保持すると問題が発生する可能性があります。

例:

package main

import "fmt"

func increment(num int) {
    num++
    fmt.Printf("Inside increment(): num = %d, address = %p \n", num, &num)
}

func main() {
    n := 21
    fmt.Printf("Before increment(): n = %d, address = %p \n", n, &n)
    increment(n)
    fmt.Printf("After increment(): n = %d, address = %p \n", n, &n)
}
ログイン後にコピー
ログイン後にコピー
ログイン後にコピー
ログイン後にコピー

このコード内:

  • データはヒープ上に割り当てられた大きなスライスです。
  • それへの参照 ([]int) を保持することで、ガベージ コレクターがメモリを解放するのを防ぎます。
  • 適切に管理しないと、メモリ使用量が増加する可能性があります。

同時実行の問題 - ポインターによるデータ競合

ポインターが直接関係する例を次に示します。

Before increment(): n = 21, address = 0xc000012070 
Inside increment(): num = 22, address = 0xc000012078 
After increment(): n = 21, address = 0xc000012070
ログイン後にコピー
ログイン後にコピー
ログイン後にコピー
ログイン後にコピー

このコードが失敗する理由:

  • 複数のゴルーチンは、同期せずにポインター counterPtr を逆参照およびインクリメントします。
  • これにより、複数のゴルーチンが同期せずに同じメモリ位置に同時にアクセスして変更するため、データ競合が発生します。操作 *counterPtr には複数のステップ (読み取り、増分、書き込み) が含まれており、スレッドセーフではありません。

データ競合の修正:

ミューテックスとの同期を追加することでこれを修正できます:

package main

import "fmt"

func incrementPointer(num *int) {
    (*num)++
    fmt.Printf("Inside incrementPointer(): num = %d, address = %p \n", *num, num)
}

func main() {
    n := 42
    fmt.Printf("Before incrementPointer(): n = %d, address = %p \n", n, &n)
    incrementPointer(&n)
    fmt.Printf("After incrementPointer(): n = %d, address = %p \n", n, &n)
}

ログイン後にコピー
ログイン後にコピー
ログイン後にコピー
ログイン後にコピー

この修正の仕組み:

  • mu.Lock() と mu.Unlock() は、一度に 1 つのゴルーチンだけがポインターにアクセスして変更することを保証します。
  • これにより、競合状態が防止され、カウンターの最終値が正しいことが保証されます。

Go の言語仕様には何と記載されていますか?

Go の言語仕様は、変数がスタックに割り当てられるかヒープに割り当てられるかを直接指示するものではないことは注目に値します。これらはランタイムとコンパイラー実装の詳細であり、Go のバージョンまたは実装間で異なる可能性のある柔軟性と最適化を可能にします。

これは次のことを意味します:

  • メモリの管理方法は、Go のバージョンが異なると異なる場合があります。
  • メモリの特定の領域に変数が割り当てられることに依存すべきではありません。
  • メモリ割り当てを制御するのではなく、明確で正しいコードを書くことに集中してください。

例:

変数がスタックに割り当てられることが予想される場合でも、コンパイラは分析に基づいて変数をヒープに移動することを決定する場合があります。

package main

import "fmt"

func increment(num int) {
    num++
    fmt.Printf("Inside increment(): num = %d, address = %p \n", num, &num)
}

func main() {
    n := 21
    fmt.Printf("Before increment(): n = %d, address = %p \n", n, &n)
    increment(n)
    fmt.Printf("After increment(): n = %d, address = %p \n", n, &n)
}
ログイン後にコピー
ログイン後にコピー
ログイン後にコピー
ログイン後にコピー

要点: メモリ割り当ての詳細は一種の内部実装であり、Go 言語仕様の一部ではないため、これらの情報は単なる一般的なガイドラインであり、後日変更される可能性がある固定ルールではありません。

パフォーマンスとメモリ使用量のバランスをとる

値渡しかポインター渡しかを決めるときは、データのサイズとパフォーマンスへの影響を考慮する必要があります。

値による大きな構造体の受け渡し:

Before increment(): n = 21, address = 0xc000012070 
Inside increment(): num = 22, address = 0xc000012078 
After increment(): n = 21, address = 0xc000012070
ログイン後にコピー
ログイン後にコピー
ログイン後にコピー
ログイン後にコピー

ポインターによる大きな構造体の受け渡し:

package main

import "fmt"

func incrementPointer(num *int) {
    (*num)++
    fmt.Printf("Inside incrementPointer(): num = %d, address = %p \n", *num, num)
}

func main() {
    n := 42
    fmt.Printf("Before incrementPointer(): n = %d, address = %p \n", n, &n)
    incrementPointer(&n)
    fmt.Printf("After incrementPointer(): n = %d, address = %p \n", n, &n)
}

ログイン後にコピー
ログイン後にコピー
ログイン後にコピー
ログイン後にコピー

考慮事項:

  • 値による受け渡しは安全で簡単ですが、大規模なデータ構造の場合は非効率的になる可能性があります。
  • ポインターによる受け渡しはコピーを回避しますが、同時実行の問題を避けるために慎重な処理が必要です。

現場での経験から:

キャリアの初期に、大規模なデータセットを処理する Go アプリケーションを最適化していたときのことを思い出します。最初は、コードの推論が簡単になると考えて、大きな構造体を値で渡しました。しかし、メモリ使用量が比較的高く、ガベージ コレクションが頻繁に停止していることに偶然気づきました。

先輩とのペア プログラミングで Go の pprof ツールを使用してアプリケーションをプロファイリングした後、大きな構造体のコピーがボトルネックであることがわかりました。値の代わりにポインターを渡すようにコードをリファクタリングしました。これによりメモリ使用量が削減され、パフォーマンスが大幅に向上しました。

しかし、この変化には課題がなかったわけではありません。複数の goroutine が共有データにアクセスするようになったため、コードがスレッドセーフであることを確認する必要がありました。ミューテックスを使用した同期を実装し、潜在的な競合状態がないかコードを慎重にレビューしました。

得られた教訓: Go がメモリ割り当てを処理する方法を早い段階で理解すると、パフォーマンスの向上とコードの安全性および保守性のバランスをとることが不可欠であるため、より効率的なコードを作成するのに役立ちます。

最終的な考え

Go のメモリ管理へのアプローチ (他の場所で行われている方法と同様) は、パフォーマンスとシンプルさの間のバランスを保っています。多くの低レベルの詳細を抽象化することで、開発者は手動のメモリ管理に行き詰まることなく、堅牢なアプリケーションの構築に集中できるようになります。

覚えておくべき重要なポイント:

  • 値による受け渡しは簡単ですが、大規模なデータ構造の場合は非効率的になる可能性があります。
  • ポインターを使用するとパフォーマンスが向上しますが、データ競合などの問題を避けるために慎重な取り扱いが必要です。
  • エスケープ分析は、変数がスタックまたはヒープに割り当てられるかどうかを決定しますが、これは内部的な詳細です。
  • ガベージ コレクションはメモリ リークの防止に役立ちますが、オーバーヘッドが発生する可能性があります。
  • 同時実行 には、共有データが関係する場合の同期が必要です。

これらの概念を念頭に置き、Go のツールを使用してコードのプロファイリングと分析を行うことで、効率的で安全なアプリケーションを作成できます。


ポインタを使用した Go のメモリ管理のこの探索がお役に立てば幸いです。 Go を始めたばかりの場合でも、理解を深めたい場合でも、コードを試してコンパイラーとランタイムがどのように動作するかを観察することは、優れた学習方法です。

あなたの経験や質問があれば、お気軽に共有してください。私は常に Go について議論し、学び、書きたいと思っています。

ボーナス コンテンツ - ダイレクト ポインターのサポート

知っていますか?ポインターは、特定のデータ型に対して直接作成できる場合と、直接作成できない場合があります。この短い表ではそれらについて説明します。


Type Supports Direct Pointer Creation? Example
Structs ✅ Yes p := &Person{Name: "Alice", Age: 30}
Arrays ✅ Yes arrPtr := &[3]int{1, 2, 3}
Slices ❌ No (indirect via variable) slice := []int{1, 2, 3}; slicePtr := &slice
Maps ❌ No (indirect via variable) m := map[string]int{}; mPtr := &m
Channels ❌ No (indirect via variable) ch := make(chan int); chPtr := &ch
Basic Types ❌ No (requires a variable) val := 42; p := &val
time.Time (Struct) ✅ Yes t := &time.Time{}
Custom Structs ✅ Yes point := &Point{X: 1, Y: 2}
Interface Types ✅ Yes (but rarely needed) var iface interface{} = "hello"; ifacePtr := &iface
time.Duration (Alias of int64) ❌ No duration := time.Duration(5); p := &duration
タイプ 直接ポインターの作成をサポートしますか? 例 構造体 ✅ はい p := &パーソン{名前: "アリス"、年齢: 30} 配列 ✅ はい arrPtr := &[3]int{1, 2, 3} スライス ❌ いいえ (変数経由で間接的に) スライス := []int{1, 2, 3};スライスPtr := &スライス 地図 ❌ いいえ (変数経由で間接的に) m := マップ[文字列]int{}; mPtr := &m チャンネル ❌ いいえ (変数経由で間接的に) ch := make(chan int); chPtr := &ch 基本タイプ ❌ いいえ (変数が必要です) val := 42; p := &val time.Time (構造体) ✅ はい t := &time.Time{} カスタム構造体 ✅ はい ポイント := &ポイント{X: 1, Y: 2} インターフェースの種類 ✅ はい (ただし、必要になることはほとんどありません) var iface インターフェース{} = "hello"; ifacePtr := &iface time.Duration (int64 のエイリアス) ❌ いいえ 期間 := time.Duration(5); p := &duration テーブル>

これが気に入ったら、コメントでお知らせください。今後、記事にこのようなおまけコンテンツを追加していきたいと思います。

読んでいただきありがとうございます!さらに詳しい内容については、以下をご検討ください。

コードをお届けします:)

私のソーシャル リンク: LinkedIn |ギットハブ | ? (旧Twitter) |サブスタック |開発者 |ハッシュノード

以上がGo: ポインタとメモリ管理の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。

ソース:dev.to
このウェブサイトの声明
この記事の内容はネチズンが自主的に寄稿したものであり、著作権は原著者に帰属します。このサイトは、それに相当する法的責任を負いません。盗作または侵害の疑いのあるコンテンツを見つけた場合は、admin@php.cn までご連絡ください。
著者別の最新記事
人気のチュートリアル
詳細>
最新のダウンロード
詳細>
ウェブエフェクト
公式サイト
サイト素材
フロントエンドテンプレート