ホームページ > バックエンド開発 > Golang > Go でのメモリ割り当てについて説明した記事

Go でのメモリ割り当てについて説明した記事

リリース: 2023-07-25 13:57:32
転載
1073 人が閲覧しました

#今日は、Go のメモリ管理に関する一般的な知識ポイントをいくつか共有します。

# 1. メモリ割り当ての 3 つの主要なコンポーネント

Go でのメモリ割り当てのプロセスは次のとおりです。主に 3 つで構成されます。大きなコンポーネントによって管理されるレベルは、上から下まで次のとおりです。

mheap

プログラムが開始されると、Go はまず大きなメモリを申請します。オペレーティング システムからの情報を取得し、

mheap 構造のグローバル管理に委ねられます。

具体的にはどのように管理すればよいのでしょうか? mheap はこの大きなメモリを、mspan と呼ばれる仕様の異なる小さなメモリ ブロックに分割します。mspan は仕様の大きさに応じて約 70 種類あります。この分割は非常に細かく、要求を満たすのに十分であると言えます。さまざまなオブジェクトメモリのニーズと配布。

このように大小さまざまなmspan仕様が混在していて、管理が大変なのではないでしょうか?

つまり、次のレベルのコンポーネント mcentral があります

mcentral

Go プログラムを開始すると、多くの mcentral が初期化されます。各 mcentral は、次のレベルのコンポーネントのみを担当します。特定の仕様の mspan を管理します。

mheap に基づいた mspan の洗練された管理を実装する mcentral と同等。

しかし、mcentral は Go プログラム内でグローバルに表示されるため、コルーチンがメモリを適用するために mcentral に来るたびに、ロックする必要があります。

各コルーチンがメモリを申請するために mcentral に来る場合、頻繁なロックと解放によるオーバーヘッドが非常に大きくなることが予想されます。

したがって、この圧力を緩衝するために mcentral のセカンダリ プロキシが必要です

mcache

Go プログラムでは、各スレッド M が単一の粒度でプロセッサ P にバインドされます。 goroutine はマルチ処理中に実行でき、各 Pmcache というローカル キャッシュにバインドされます。

メモリ割り当てが必要な場合、現在実行中の ゴルーチンは、利用可能な mspanmcache から探します。ローカルの mcache からメモリを割り当てるときにロックする必要はありません。この割り当て戦略はより効率的です。

mspan サプライ チェーン

mcache の mspan の数は必ずしも十分ではありません。供給が需要を超えると、mcache は mcentral からより多くの mspan を再度申請します。 、mcentral の mspan の数が十分でない場合、mcentral は上位の mheap からの mspan も適用します。もっと極端に言えば、mheap の mspan がプログラムのメモリ要求を満たすことができない場合はどうすればよいでしょうか?

他に方法はありません。mheap は、恥知らずにもオペレーティング システムの兄貴分にのみ適用できます。

Go でのメモリ割り当てについて説明した記事

上記の供給プロセスは、メモリ ブロックが 64KB 未満のシナリオにのみ適用できます。その理由は、Go がワーカー スレッド mcache のローカル キャッシュとグローバル中央キャッシュ を使用できないためです。 mcentral は 64KB を超えるメモリ割り当てを管理するため、64KB を超えるメモリ アプリケーションの場合、対応する数のメモリ ページ (各ページ サイズは 8KB) がヒープ (mheap) から直接割り当てられます。プログラム。

# 2. ヒープ メモリとスタック メモリとは何ですか?

さまざまなメモリ管理 (割り当てとリサイクル) 方法に従って、メモリは ヒープ メモリ スタック メモリ に分類できます。

それでは、それらの違いは何でしょうか?

ヒープ メモリ: メモリ アロケータとガベージ コレクタは、リサイクルを担当します。

スタック メモリ: コンパイラによって自動的に割り当ておよび解放されます

プログラムの実行中は複数のスタック メモリが存在する可能性がありますが、ヒープ メモリは確実に 1 つだけです。

各スタック メモリはスレッドまたはコルーチンによって独立して占有されるため、スタックからのメモリ割り当てにはロックが必要なく、関数終了後にスタック メモリは自動的にリサイクルされ、パフォーマンスはヒープメモリ。

では、ヒープ メモリについてはどうでしょうか?複数のスレッドまたはコルーチンが同時にヒープからメモリを申請する可能性があるため、ヒープ内のメモリを申請するには競合を避けるためにロックする必要があり、関数終了後にヒープ メモリには GC (ガベージ コレクション) の介入が必要です。 GC 操作の回数が多いと、プログラムのパフォーマンスが大幅に低下します。

# 3. エスケープ解析の必要性

プログラムのパフォーマンスを向上させるためには、メモリ内のメモリが必要であることがわかります。ヒープを最小化する必要があります。これにより、GC への負担が軽減されます。

変数にメモリがヒープ上に割り当てられるかスタック上に割り当てられるかを決定する際、先人がいくつかのルールをまとめましたが、コーディング時にこの問題に常に注意を払うのはプログラマの責任です。プログラマの要件は次のとおりです。かなり高い。

幸いなことに、Go コンパイラにはエスケープ解析機能も用意されており、エスケープ解析を使用すると、プログラマがヒープ上に割り当てたすべての変数を直接検出できます (この現象はエスケープと呼ばれます)。

方法は以下のコマンドを実行することです

go build -gcflags '-m -l' demo.go 

# 或者再加个 -m 查看更详细信息
go build -gcflags '-m -m -l' demo.go
ログイン後にコピー

#メモリ割り当て位置のルール

分析ツールをエスケープすると、実際にどの変数がヒープに割り当てられているかを手動で決定できます。

それでは、これらのルールとは何でしょうか?

#変数の使用範囲により、主に以下の 4 つの状況が考えられます。

    #変数の種類に基づいて決定
  1. 変数の占有サイズに基づいて
  2. ##基づいて変数の長さは決まりますか?
  3. 次に 1 つずつ分析して検証します
  4. According変数の使用範囲へ

    #コンパイル時にコンパイラはエスケープ解析を行い、変数が関数内でのみ使用されていることが判明した場合、その変数にメモリを割り当てることができます。スタック。
たとえば、以下の例では、

func foo() int {
    v := 1024
    return v
}

func main() {
    m := foo()
    fmt.Println(m)
}
ログイン後にコピー

go build -gcflags '-m -l' Demon.go を通じてエスケープ分析の結果を表示できます。 -m

はエスケープ解析情報を出力し、

-l

はインライン最適化を無効にします。

分析結果から、v 変数に関するエスケープ命令は見られませんでした。これは、変数がエスケープされず、スタックに割り当てられたことを示しています。

$ go build -gcflags '-m -l' demo.go 
# command-line-arguments
./demo.go:12:13: ... argument does not escape
./demo.go:12:13: m escapes to heap
ログイン後にコピー
変数を関数のスコープ外で使用する必要があり、変数がまだスタックに割り当てられている場合、関数が戻ると、変数が指すメモリ空間がリサイクルされます。プログラムは必然的にエラーを報告するため、そのような変数はヒープ上にのみ割り当てることができます。 たとえば、以下の例では、 はポインタを返します
func foo() *int {
    v := 1024
    return &v
}

func main() {
    m := foo()
    fmt.Println(*m) // 1024
}
ログイン後にコピー

エスケープ解析の結果から、

moved to heap: v## であることがわかります。 # , v 変数はヒープから割り当てられたメモリーであり、上記のシナリオとは明らかに異なります。

$ go build -gcflags '-m -l' demo.go 
# command-line-arguments
./demo.go:6:2: moved to heap: v
./demo.go:12:13: ... argument does not escape
./demo.go:12:14: *m escapes to heap
ログイン後にコピー

除了返回指针之外,还有其他的几种情况也可归为一类:

第一种情况:返回任意引用型的变量:Slice 和 Map

func foo() []int {
    a := []int{1,2,3}
    return a
}

func main() {
    b := foo()
    fmt.Println(b)
}
ログイン後にコピー

逃逸分析结果

$ go build -gcflags '-m -l' demo.go 
# command-line-arguments
./demo.go:6:12: []int literal escapes to heap
./demo.go:12:13: ... argument does not escape
./demo.go:12:13: b escapes to heap
ログイン後にコピー

第二种情况:在闭包函数中使用外部变量

func Increase() func() int {
    n := 0
    return func() int {
        n++
        return n
    }
}

func main() {
    in := Increase()
    fmt.Println(in()) // 1
    fmt.Println(in()) // 2
}
ログイン後にコピー

逃逸分析结果

$ go build -gcflags '-m -l' demo.go 
# command-line-arguments
./demo.go:6:2: moved to heap: n
./demo.go:7:9: func literal escapes to heap
./demo.go:15:13: ... argument does not escape
./demo.go:15:16: in() escapes to heap
ログイン後にコピー

根据变量类型是否确定

在上边例子中,也许你发现了,所有编译输出的最后一行中都是 m escapes to heap

奇怪了,为什么 m 会逃逸到堆上?

其实就是因为我们调用了 fmt.Println() 函数,它的定义如下

func Println(a ...interface{}) (n int, err error) {
    return Fprintln(os.Stdout, a...)
}
ログイン後にコピー

可见其接收的参数类型是 interface{} ,对于这种编译期不能确定其参数的具体类型,编译器会将其分配于堆上。

根据变量的占用大小

最开始的时候,就介绍到,以 64KB 为分界线,我们将内存块分为 小内存块 和 大内存块。

小内存块走常规的 mspan 供应链申请,而大内存块则需要直接向 mheap,在堆区申请。

以下的例子来说明

func foo() {
    nums1 := make([]int, 8191) // < 64KB
    for i := 0; i < 8191; i++ {
        nums1[i] = i
    }
}

func bar() {
    nums2 := make([]int, 8192) // = 64KB
    for i := 0; i < 8192; i++ {
        nums2[i] = i
    }
}
ログイン後にコピー

-gcflags 多加个 -m 可以看到更详细的逃逸分析的结果

$ go build -gcflags &#39;-m -l&#39; demo.go 
# command-line-arguments
./demo.go:5:15: make([]int, 8191) does not escape
./demo.go:12:15: make([]int, 8192) escapes to heap
ログイン後にコピー

那为什么是 64 KB 呢?

我只能说是试出来的 (8191刚好不逃逸,8192刚好逃逸),网上有很多文章千篇一律的说和 ulimit -a 中的 stack size 有关,但经过了解这个值表示的是系统栈的最大限制是 8192 KB,刚好是 8M。

$ ulimit -a
-t: cpu time (seconds)              unlimited
-f: file size (blocks)              unlimited
-d: data seg size (kbytes)          unlimited
-s: stack size (kbytes)             8192
ログイン後にコピー

我个人实在无法理解这个 8192 (8M) 和 64 KB 是如何对应上的,如果有朋友知道,还请指教一下。

根据变量长度是否确定

由于逃逸分析是在编译期就运行的,而不是在运行时运行的。因此避免有一些不定长的变量可能会很大,而在栈上分配内存失败,Go 会选择把这些变量统一在堆上申请内存,这是一种可以理解的保险的做法。

func foo() {
    length := 10
    arr := make([]int, 0 ,length)  // 由于容量是变量,因此不确定,因此在堆上申请
}

func bar() {
    arr := make([]int, 0 ,10)  // 由于容量是常量,因此是确定的,因此在栈上申请
}
ログイン後にコピー

以上がGo でのメモリ割り当てについて説明した記事の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。

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