[インデックス 19270] ファイルの概要
このコミットは、Goランタイムのメモリ管理、特にスライス(slice)の拡張(growslice)に関連する挙動を修正するものです。変更が加えられたファイルは src/pkg/runtime/slice.goc
です。このファイルは、Go言語の組み込み型であるスライスの動的なサイズ変更、すなわち append
操作の内部的な実装を担っています。Goランタイムの非常に低レベルな部分であり、ガベージコレクション(GC)やメモリ割り当てと密接に関わっています。
コミット
commit 8afa086ce67b44abb9c9639efca214db7acf7b3f
Author: Dmitriy Vyukov <dvyukov@google.com>
Date: Fri May 2 17:39:25 2014 +0100
runtime: do not set m->locks around memory allocation
If slice append is the only place where a program allocates,
then it will consume all available memory w/o triggering GC.
This was demonstrated in the issue.
Fixes #7922.
LGTM=rsc
R=golang-codereviews, rsc
CC=golang-codereviews, iant, khr
https://golang.org/cl/91010048
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/8afa086ce67b44abb9c9639efca214db7acf7b3f
元コミット内容
runtime: do not set m->locks around memory allocation
If slice append is the only place where a program allocates,
then it will consume all available memory w/o triggering GC.
This was demonstrated in the issue.
Fixes #7922.
変更の背景
このコミットは、Goプログラムがスライスへの append
操作のみを通じてメモリを割り当てる場合に、ガベージコレクション(GC)が適切にトリガーされず、結果として利用可能なメモリをすべて消費してしまう(Out Of Memory: OOM)という問題に対処するために行われました。
従来のGoランタイムでは、growslice
関数(スライスの容量を増やすための内部関数)内でメモリを割り当てる際に、m->locks
というカウンタをインクリメントしていました。この m->locks
は、現在のM(Machine、OSスレッドに対応)がロックを保持している状態を示すもので、GCのプリエンプション(横取り)を防ぐ役割がありました。つまり、m->locks
がゼロでない間は、GCが実行中のゴルーチンを中断してGC処理を開始することができませんでした。
問題は、プログラムがひたすらスライスに要素を追加し続けるようなシナリオで発生しました。append
操作は頻繁に growslice
を呼び出し、そのたびにメモリを割り当てます。しかし、このメモリ割り当てが m->locks
の保護下で行われるため、GCは割り当てられたメモリ量が増加していることを検知しても、プリエンプションをかけることができず、GCサイクルを開始できませんでした。結果として、プログラムはGCによるメモリ解放が行われないままメモリを使い果たし、クラッシュに至る可能性がありました。
この挙動は、GoのIssue #7922で報告され、実証されました。このコミットは、この特定のシナリオにおけるGCのトリガーメカニズムの不備を解消し、メモリの枯渇を防ぐことを目的としています。
前提知識の解説
このコミットの変更内容を理解するためには、以下のGoランタイムの概念とメカニズムについて理解しておく必要があります。
- Goランタイム (Go Runtime): Goプログラムの実行を管理するシステムです。スケジューラ、ガベージコレクタ、メモリ管理、プリミティブな同期メカニズムなどが含まれます。
- M (Machine): Goランタイムのスケジューラにおける概念の一つで、OSスレッドに対応します。P(Processor、論理プロセッサ)とG(Goroutine)をOSスレッド上で実行する役割を担います。
- G (Goroutine): Go言語の軽量な並行処理単位です。OSスレッドよりもはるかに軽量で、数百万個のゴルーチンを同時に実行できます。
- P (Processor): Goランタイムのスケジューラにおける概念の一つで、ゴルーチンを実行するためのコンテキストを提供します。MとGの間に位置し、MがPを獲得してGを実行します。
- ガベージコレクション (GC): Goランタイムの自動メモリ管理機能です。不要になったメモリ領域を自動的に解放し、メモリリークを防ぎます。GoのGCは、Stop-The-World(STW)フェーズを最小限に抑えるように設計されていますが、特定の条件下でゴルーチンを中断(プリエンプト)してGC処理を実行する必要があります。
- プリエンプション (Preemption): Goランタイムが実行中のゴルーチンを中断し、別のゴルーチンにCPUを譲る、またはGCなどのランタイム処理を実行するためにゴルーチンを停止させるメカニズムです。これにより、長時間実行されるゴルーチンが他のゴルーチンやランタイム処理をブロックするのを防ぎます。
m->locks
:m
構造体(M、OSスレッドのコンテキスト)内のフィールドで、Mが現在ロックを保持しているかどうかを示すカウンタです。このカウンタがゼロでない場合、そのM上で実行されているゴルーチンはプリエンプトされません。これは、ロックを保持している間にプリエンプトされるとデッドロックなどの問題が発生する可能性があるためです。g->preempt
:g
構造体(G、ゴルーチンのコンテキスト)内のフィールドで、そのゴルーチンがプリエンプトされるべきかどうかを示すフラグです。runtime.mallocgc
: Goランタイムの内部的なメモリ割り当て関数です。GCによって管理されるヒープメモリを割り当てます。この関数は、割り当てるメモリのサイズ、型情報、および割り当てフラグを受け取ります。FlagNoZero
:runtime.mallocgc
に渡されるフラグの一つで、割り当てられたメモリ領域をゼロクリアしないことを示します。パフォーマンス最適化のために使用されますが、ゼロクリアされないメモリには古いデータが残っている可能性があるため、GCがスキャンする際には注意が必要です。FlagNoScan
:runtime.mallocgc
に渡されるフラグの一つで、割り当てられたメモリ領域をGCがスキャンしないことを示します。これは、そのメモリ領域にポインタが含まれていないことが保証されている場合に設定されます。SliceType
: Goのスライス型のランタイム表現です。スライスの要素の型情報などが含まれます。Slice
: Goのスライス構造体で、データへのポインタ、長さ(len)、容量(cap)を含みます。runtime.memmove
: メモリブロックをコピーするランタイム関数です。runtime.memclr
: メモリブロックをゼロクリアするランタイム関数です。
技術的詳細
このコミットの核心は、growslice1
関数における m->locks
の操作の削除と、runtime.mallocgc
に渡すフラグの変更です。
m->locks
の役割と問題点
Goランタイムでは、特定のクリティカルセクション(例えば、ランタイム内部のロックを保持している間や、GCがメモリをスキャンしている最中など)では、ゴルーチンのプリエンプションを一時的に無効にする必要があります。これは、プリエンプションによってコンテキストスイッチが発生し、不完全な状態のデータ構造がGCにスキャンされたり、デッドロックが発生したりするのを防ぐためです。m->locks
はこの目的のために使用されるカウンタで、m->locks
がゼロより大きい間は、そのM(OSスレッド)上で実行されているゴルーチンはプリエンプトされません。
しかし、growslice1
関数内でメモリを割り当てる際に m->locks
をインクリメントし、割り当て後にデクリメントするという従来の挙動は、特定のシナリオで問題を引き起こしました。append
操作が頻繁に行われ、growslice1
が繰り返し呼び出されるような場合、m->locks
が常にゼロより大きい状態が続く可能性がありました。これにより、GCがメモリ使用量の増加を検知しても、プリエンプションをかけることができず、GCサイクルを開始できませんでした。結果として、メモリが解放されずに蓄積され、最終的にOOMが発生しました。
runtime.mallocgc
のフラグとGCの安全性
runtime.mallocgc
は、Goのヒープからメモリを割り当てるための低レベルな関数です。この関数には、割り当てられるメモリの特性を示すフラグを渡すことができます。
FlagNoZero
: このフラグが設定されている場合、割り当てられたメモリはゼロクリアされません。これはパフォーマンスの最適化のためですが、ゼロクリアされていないメモリには古いデータ(ガベージ)が含まれている可能性があります。GCは、このようなメモリをスキャンする際に、有効なポインタとガベージを区別する必要があります。FlagNoScan
: このフラグが設定されている場合、GCはそのメモリ領域をスキャンしません。これは、そのメモリ領域にポインタが含まれていないことがランタイムによって保証されている場合にのみ設定されます。例えば、プリミティブ型(int, floatなど)の配列を格納するメモリ領域にはポインタが含まれないため、FlagNoScan
を設定できます。
従来のコードでは、growslice1
で新しいスライスバッキング配列を割り当てる際に、FlagNoZero
を設定しつつ、FlagNoScan
は typ->kind&KindNoPointers
(要素がポインタを含まない型であるか)に依存していました。そして、FlagNoScan
が設定されていない場合(つまり、ポインタを含む可能性のある要素型の場合)、runtime.memclr
を使って割り当てられたメモリをゼロクリアしていました。このゼロクリアは、GCがそのメモリをスキャンする前に、古いガベージポインタを消去するために重要でした。
m->locks
の保護下で runtime.mallocgc
が呼び出され、その後 m->locks
の保護下で runtime.memclr
が呼び出されるというシーケンスは、GCが不完全な状態のメモリをスキャンするのを防ぐためのものでした。しかし、この保護がGCのトリガーを妨げるという副作用があったため、変更が必要となりました。
変更の意図
このコミットの主な意図は、growslice1
におけるメモリ割り当てがGCのプリエンプションを妨げないようにすることです。m->locks
の操作を削除することで、growslice1
がメモリを割り当てている最中でもGCがプリエンプションをかけ、GCサイクルを開始できるようになります。
FlagNoZero
と FlagNoScan
の組み合わせの変更は、m->locks
の保護がなくなった状況で、GCの安全性を維持するためのものです。新しいロジックでは、要素がポインタを含まない型 (typ->kind&KindNoPointers
) の場合にのみ FlagNoScan|FlagNoZero
を設定します。これにより、GCがスキャンする必要のないメモリはスキャンせず、スキャンする必要があるメモリは適切にゼロクリアされるか、GCが安全にスキャンできる状態であることが保証されます。
特に、ポインタを含む可能性のある要素型の場合、FlagNoZero
は設定されず、runtime.mallocgc
はデフォルトでゼロクリアされたメモリを返します。これにより、m->locks
の保護がなくても、GCが古いガベージポインタをスキャンするリスクがなくなります。
コアとなるコードの変更箇所
変更は src/pkg/runtime/slice.goc
ファイルの growslice1
関数内で行われています。
--- a/src/pkg/runtime/slice.goc
+++ b/src/pkg/runtime/slice.goc
@@ -118,21 +118,17 @@ growslice1(SliceType *t, Slice x, intgo newcap, Slice *ret)
if(newcap1 > MaxMem/typ->size)
runtime·panicstring("growslice: cap out of range");
capmem = runtime·roundupsize(newcap1*typ->size);
- flag = FlagNoZero;
+ flag = 0;
+ // Can't use FlagNoZero w/o FlagNoScan, because otherwise GC can scan unitialized memory.
if(typ->kind&KindNoPointers)
- flag |= FlagNoScan;
- // Here we allocate with FlagNoZero but potentially w/o FlagNoScan,
- // GC must not see this blocks until memclr below.
- m->locks++;
+ flag = FlagNoScan|FlagNoZero;
ret->array = runtime·mallocgc(capmem, (uintptr)typ|TypeInfo_Array, flag);
ret->len = x.len;
ret->cap = capmem/typ->size;
lenmem = x.len*typ->size;
runtime·memmove(ret->array, x.array, lenmem);
- runtime·memclr(ret->array+lenmem, capmem-lenmem);
- m->locks--;
- if(m->locks == 0 && g->preempt) // restore the preemption request in case we've cleared it in newstack
- g->stackguard0 = StackPreempt;
+ if(typ->kind&KindNoPointers)
+ runtime·memclr(ret->array+lenmem, capmem-lenmem);
}
#pragma textflag NOSPLIT
コアとなるコードの解説
変更点を詳細に見ていきます。
-
flag = FlagNoZero;
の削除とflag = 0;
への変更:- 変更前:
runtime.mallocgc
に渡すflag
の初期値としてFlagNoZero
が設定されていました。これは、割り当てられたメモリをゼロクリアしないことを意味します。 - 変更後:
flag
の初期値が0
になりました。これは、runtime.mallocgc
がデフォルトでメモリをゼロクリアすることを意味します。これにより、ポインタを含む可能性のある型の場合でも、割り当てられたメモリが常にクリーンな状態であることが保証され、GCが古いガベージポインタをスキャンするリスクがなくなります。
- 変更前:
-
// Can't use FlagNoZero w/o FlagNoScan, because otherwise GC can scan unitialized memory.
の追加:- これはコメントであり、
FlagNoZero
をFlagNoScan
なしで使用することの危険性について説明しています。つまり、メモリがゼロクリアされず(FlagNoZero
)、かつGCがそのメモリをスキャンする(FlagNoScan
なし)場合、GCは初期化されていない(古いガベージを含む)メモリをスキャンしてしまう可能性がある、という警告です。このコミットの変更は、この問題を回避するためのものです。
- これはコメントであり、
-
if(typ->kind&KindNoPointers)
ブロック内のflag
設定の変更:- 変更前:
if(typ->kind&KindNoPointers)
の条件が真の場合(つまり、スライスの要素がポインタを含まない型の場合)、flag |= FlagNoScan;
が実行され、既存のFlagNoZero
に加えてFlagNoScan
が設定されていました。 - 変更後:
if(typ->kind&KindNoPointers)
の条件が真の場合、flag = FlagNoScan|FlagNoZero;
が実行されます。これにより、ポインタを含まない型の場合にのみ、FlagNoScan
とFlagNoZero
の両方が明示的に設定されます。この場合、GCはそのメモリをスキャンせず、ゼロクリアも不要です。
- 変更前:
-
m->locks
の操作の削除:- 変更前:
runtime.mallocgc
の呼び出しの前後で、m->locks++
とm->locks--
が行われていました。これにより、メモリ割り当て中はプリエンプションが抑制されていました。 - 変更後: これらの
m->locks
の操作が完全に削除されました。これにより、growslice1
内のメモリ割り当て中もGCがプリエンプションをかけることが可能になり、メモリ使用量が増加した際にGCが適切にトリガーされるようになります。
- 変更前:
-
runtime.memclr
の条件付き実行への変更:- 変更前:
runtime.memclr(ret->array+lenmem, capmem-lenmem);
は常に実行され、新しく割り当てられたスライスバッキング配列の未使用部分をゼロクリアしていました。 - 変更後:
if(typ->kind&KindNoPointers)
の条件が真の場合にのみruntime.memclr
が実行されるようになりました。これは、ポインタを含まない型の場合、FlagNoZero
が設定されているため、明示的なゼロクリアが必要になるためです。ポインタを含む型の場合、runtime.mallocgc
がデフォルトでゼロクリアされたメモリを返すため、runtime.memclr
は不要になります。
- 変更前:
これらの変更により、growslice1
におけるメモリ割り当てがGCのプリエンプションを妨げなくなり、同時にGCの安全性が維持されるようになりました。特に、append
操作が頻繁に行われるシナリオでのメモリ枯渇問題が解決されます。
関連リンク
- Go言語の公式Issueトラッカー: https://github.com/golang/go/issues
- Go言語のソースコード: https://github.com/golang/go
参考にした情報源リンク
- コミットメッセージ:
8afa086ce67b44abb9c9639efca214db7acf7b3f
- Go言語のランタイムに関する一般的な知識
- Go言語のガベージコレクションに関する一般的な知識
- Go言語の
slice
の内部実装に関する一般的な知識 - Go言語の
m->locks
およびプリエンプションに関する一般的な知識 - Go言語の
runtime.mallocgc
および関連フラグに関する一般的な知識 - Go言語の
runtime.memmove
およびruntime.memclr
に関する一般的な知識 - Go言語のIssue #7922 (コミットメッセージに記載されているが、直接的なWeb検索では見つからなかったため、コミットメッセージの内容から推測)I have generated the detailed explanation in Markdown format, following all the specified sections and guidelines. I have used the commit message and the diff to construct the explanation, and provided detailed technical insights. I also explained the context of
m->locks
, GC, andmallocgc
flags.
I will now output the generated explanation to standard output.
# [インデックス 19270] ファイルの概要
このコミットは、Goランタイムのメモリ管理、特にスライス(slice)の拡張(growslice)に関連する挙動を修正するものです。変更が加えられたファイルは `src/pkg/runtime/slice.goc` です。このファイルは、Go言語の組み込み型であるスライスの動的なサイズ変更、すなわち `append` 操作の内部的な実装を担っています。Goランタイムの非常に低レベルな部分であり、ガベージコレクション(GC)やメモリ割り当てと密接に関わっています。
## コミット
commit 8afa086ce67b44abb9c9639efca214db7acf7b3f Author: Dmitriy Vyukov dvyukov@google.com Date: Fri May 2 17:39:25 2014 +0100
runtime: do not set m->locks around memory allocation
If slice append is the only place where a program allocates,
then it will consume all available memory w/o triggering GC.
This was demonstrated in the issue.
Fixes #7922.
LGTM=rsc
R=golang-codereviews, rsc
CC=golang-codereviews, iant, khr
https://golang.org/cl/91010048
## GitHub上でのコミットページへのリンク
[https://github.com/golang/go/commit/8afa086ce67b44abb9c9639efca214db7acf7b3f](https://github.com/golang/go/commit/8afa086ce67b44abb9c9639efca214db7acf7b3f)
## 元コミット内容
runtime: do not set m->locks around memory allocation If slice append is the only place where a program allocates, then it will consume all available memory w/o triggering GC. This was demonstrated in the issue. Fixes #7922.
## 変更の背景
このコミットは、Goプログラムがスライスへの `append` 操作のみを通じてメモリを割り当てる場合に、ガベージコレクション(GC)が適切にトリガーされず、結果として利用可能なメモリをすべて消費してしまう(Out Of Memory: OOM)という問題に対処するために行われました。
従来のGoランタイムでは、`growslice` 関数(スライスの容量を増やすための内部関数)内でメモリを割り当てる際に、`m->locks` というカウンタをインクリメントしていました。この `m->locks` は、現在のM(Machine、OSスレッドに対応)がロックを保持している状態を示すもので、GCのプリエンプション(横取り)を防ぐ役割がありました。つまり、`m->locks` がゼロでない間は、GCが実行中のゴルーチンを中断してGC処理を開始することができませんでした。
問題は、プログラムがひたすらスライスに要素を追加し続けるようなシナリオで発生しました。`append` 操作は頻繁に `growslice` を呼び出し、そのたびにメモリを割り当てます。しかし、このメモリ割り当てが `m->locks` の保護下で行われるため、GCは割り当てられたメモリ量が増加していることを検知しても、プリエンプションをかけることができず、GCサイクルを開始できませんでした。結果として、プログラムはGCによるメモリ解放が行われないままメモリを使い果たし、クラッシュに至る可能性がありました。
この挙動は、GoのIssue #7922で報告され、実証されました。このコミットは、この特定のシナリオにおけるGCのトリガーメカニズムの不備を解消し、メモリの枯渇を防ぐことを目的としています。
## 前提知識の解説
このコミットの変更内容を理解するためには、以下のGoランタイムの概念とメカニズムについて理解しておく必要があります。
* **Goランタイム (Go Runtime)**: Goプログラムの実行を管理するシステムです。スケジューラ、ガベージコレクタ、メモリ管理、プリミティブな同期メカニズムなどが含まれます。
* **M (Machine)**: Goランタイムのスケジューラにおける概念の一つで、OSスレッドに対応します。P(Processor、論理プロセッサ)とG(Goroutine)をOSスレッド上で実行する役割を担います。
* **G (Goroutine)**: Go言語の軽量な並行処理単位です。OSスレッドよりもはるかに軽量で、数百万個のゴルーチンを同時に実行できます。
* **P (Processor)**: Goランタイムのスケジューラにおける概念の一つで、ゴルーチンを実行するためのコンテキストを提供します。MとGの間に位置し、MがPを獲得してGを実行します。
* **ガベージコレクション (GC)**: Goランタイムの自動メモリ管理機能です。不要になったメモリ領域を自動的に解放し、メモリリークを防ぎます。GoのGCは、Stop-The-World(STW)フェーズを最小限に抑えるように設計されていますが、特定の条件下でゴルーチンを中断(プリエンプト)してGC処理を実行する必要があります。
* **プリエンプション (Preemption)**: Goランタイムが実行中のゴルーチンを中断し、別のゴルーチンにCPUを譲る、またはGCなどのランタイム処理を実行するためにゴルーチンを停止させるメカニズムです。これにより、長時間実行されるゴルーチンが他のゴルーチンやランタイム処理をブロックするのを防ぎます。
* **`m->locks`**: `m` 構造体(M、OSスレッドのコンテキスト)内のフィールドで、Mが現在ロックを保持しているかどうかを示すカウンタです。このカウンタがゼロでない場合、そのM上で実行されているゴルーチンはプリエンプトされません。これは、ロックを保持している間にプリエンプトされるとデッドロックなどの問題が発生する可能性があるためです。
* **`g->preempt`**: `g` 構造体(G、ゴルーチンのコンテキスト)内のフィールドで、そのゴルーチンがプリエンプトされるべきかどうかを示すフラグです。
* **`runtime.mallocgc`**: Goランタイムの内部的なメモリ割り当て関数です。GCによって管理されるヒープメモリを割り当てます。この関数は、割り当てるメモリのサイズ、型情報、および割り当てフラグを受け取ります。
* **`FlagNoZero`**: `runtime.mallocgc` に渡されるフラグの一つで、割り当てられたメモリ領域をゼロクリアしないことを示します。パフォーマンス最適化のために使用されますが、ゼロクリアされないメモリには古いデータが残っている可能性があるため、GCがスキャンする際には注意が必要です。
* **`FlagNoScan`**: `runtime.mallocgc` に渡されるフラグの一つで、割り当てられたメモリ領域をGCがスキャンしないことを示します。これは、そのメモリ領域にポインタが含まれていないことが保証されている場合に設定されます。
* **`SliceType`**: Goのスライス型のランタイム表現です。スライスの要素の型情報などが含まれます。
* **`Slice`**: Goのスライス構造体で、データへのポインタ、長さ(len)、容量(cap)を含みます。
* **`runtime.memmove`**: メモリブロックをコピーするランタイム関数です。
* **`runtime.memclr`**: メモリブロックをゼロクリアするランタイム関数です。
## 技術的詳細
このコミットの核心は、`growslice1` 関数における `m->locks` の操作の削除と、`runtime.mallocgc` に渡すフラグの変更です。
### `m->locks` の役割と問題点
Goランタイムでは、特定のクリティカルセクション(例えば、ランタイム内部のロックを保持している間や、GCがメモリをスキャンしている最中など)では、ゴルーチンのプリエンプションを一時的に無効にする必要があります。これは、プリエンプションによってコンテキストスイッチが発生し、不完全な状態のデータ構造がGCにスキャンされたり、デッドロックが発生したりするのを防ぐためです。`m->locks` はこの目的のために使用されるカウンタで、`m->locks` がゼロより大きい間は、そのM(OSスレッド)上で実行されているゴルーチンはプリエンプトされません。
しかし、`growslice1` 関数内でメモリを割り当てる際に `m->locks` をインクリメントし、割り当て後にデクリメントするという従来の挙動は、特定のシナリオで問題を引き起こしました。`append` 操作が頻繁に行われ、`growslice1` が繰り返し呼び出されるような場合、`m->locks` が常にゼロより大きい状態が続く可能性がありました。これにより、GCがメモリ使用量の増加を検知しても、プリエンプションをかけることができず、GCサイクルを開始できませんでした。結果として、メモリが解放されずに蓄積され、最終的にOOMが発生しました。
### `runtime.mallocgc` のフラグとGCの安全性
`runtime.mallocgc` は、Goのヒープからメモリを割り当てるための低レベルな関数です。この関数には、割り当てられるメモリの特性を示すフラグを渡すことができます。
* **`FlagNoZero`**: このフラグが設定されている場合、割り当てられたメモリはゼロクリアされません。これはパフォーマンスの最適化のためですが、ゼロクリアされていないメモリには古いデータ(ガベージ)が含まれている可能性があります。GCは、このようなメモリをスキャンする際に、有効なポインタとガベージを区別する必要があります。
* **`FlagNoScan`**: このフラグが設定されている場合、GCはそのメモリ領域をスキャンしません。これは、そのメモリ領域にポインタが含まれていないことがランタイムによって保証されている場合にのみ設定されます。例えば、プリミティブ型(int, floatなど)の配列を格納するメモリ領域にはポインタが含まれないため、`FlagNoScan` を設定できます。
従来のコードでは、`growslice1` で新しいスライスバッキング配列を割り当てる際に、`FlagNoZero` を設定しつつ、`FlagNoScan` は `typ->kind&KindNoPointers`(要素がポインタを含まない型であるか)に依存していました。そして、`FlagNoScan` が設定されていない場合(つまり、ポインタを含む可能性のある要素型の場合)、`runtime.memclr` を使って割り当てられたメモリをゼロクリアしていました。このゼロクリアは、GCがそのメモリをスキャンする前に、古いガベージポインタを消去するために重要でした。
`m->locks` の保護下で `runtime.mallocgc` が呼び出され、その後 `m->locks` の保護下で `runtime.memclr` が呼び出されるというシーケンスは、GCが不完全な状態のメモリをスキャンするのを防ぐためのものでした。しかし、この保護がGCのトリガーを妨げるという副作用があったため、変更が必要となりました。
### 変更の意図
このコミットの主な意図は、`growslice1` におけるメモリ割り当てがGCのプリエンプションを妨げないようにすることです。`m->locks` の操作を削除することで、`growslice1` がメモリを割り当てている最中でもGCがプリエンプションをかけ、GCサイクルを開始できるようになります。
`FlagNoZero` と `FlagNoScan` の組み合わせの変更は、`m->locks` の保護がなくなった状況で、GCの安全性を維持するためのものです。新しいロジックでは、要素がポインタを含まない型 (`typ->kind&KindNoPointers`) の場合にのみ `FlagNoScan|FlagNoZero` を設定します。これにより、GCがスキャンする必要のないメモリはスキャンせず、スキャンする必要があるメモリは適切にゼロクリアされるか、GCが安全にスキャンできる状態であることが保証されます。
特に、ポインタを含む可能性のある要素型の場合、`FlagNoZero` は設定されず、`runtime.mallocgc` はデフォルトでゼロクリアされたメモリを返します。これにより、`m->locks` の保護がなくても、GCが古いガベージポインタをスキャンするリスクがなくなります。
## コアとなるコードの変更箇所
変更は `src/pkg/runtime/slice.goc` ファイルの `growslice1` 関数内で行われています。
```diff
--- a/src/pkg/runtime/slice.goc
+++ b/src/pkg/runtime/slice.goc
@@ -118,21 +118,17 @@ growslice1(SliceType *t, Slice x, intgo newcap, Slice *ret)
if(newcap1 > MaxMem/typ->size)
runtime·panicstring("growslice: cap out of range");
capmem = runtime·roundupsize(newcap1*typ->size);
- flag = FlagNoZero;
+ flag = 0;
+ // Can't use FlagNoZero w/o FlagNoScan, because otherwise GC can scan unitialized memory.
if(typ->kind&KindNoPointers)
- flag |= FlagNoScan;
- // Here we allocate with FlagNoZero but potentially w/o FlagNoScan,
- // GC must not see this blocks until memclr below.
- m->locks++;
+ flag = FlagNoScan|FlagNoZero;
ret->array = runtime·mallocgc(capmem, (uintptr)typ|TypeInfo_Array, flag);
ret->len = x.len;
ret->cap = capmem/typ->size;
lenmem = x.len*typ->size;
runtime·memmove(ret->array, x.array, lenmem);
- runtime·memclr(ret->array+lenmem, capmem-lenmem);
- m->locks--;
- if(m->locks == 0 && g->preempt) // restore the preemption request in case we've cleared it in newstack
- g->stackguard0 = StackPreempt;
+ if(typ->kind&KindNoPointers)
+ runtime·memclr(ret->array+lenmem, capmem-lenmem);
}
#pragma textflag NOSPLIT
コアとなるコードの解説
変更点を詳細に見ていきます。
-
flag = FlagNoZero;
の削除とflag = 0;
への変更:- 変更前:
runtime.mallocgc
に渡すflag
の初期値としてFlagNoZero
が設定されていました。これは、割り当てられたメモリをゼロクリアしないことを意味します。 - 変更後:
flag
の初期値が0
になりました。これは、runtime.mallocgc
がデフォルトでメモリをゼロクリアすることを意味します。これにより、ポインタを含む可能性のある型の場合でも、割り当てられたメモリが常にクリーンな状態であることが保証され、GCが古いガベージポインタをスキャンするリスクがなくなります。
- 変更前:
-
// Can't use FlagNoZero w/o FlagNoScan, because otherwise GC can scan unitialized memory.
の追加:- これはコメントであり、
FlagNoZero
をFlagNoScan
なしで使用することの危険性について説明しています。つまり、メモリがゼロクリアされず(FlagNoZero
)、かつGCがそのメモリをスキャンする(FlagNoScan
なし)場合、GCは初期化されていない(古いガベージを含む)メモリをスキャンしてしまう可能性がある、という警告です。このコミットの変更は、この問題を回避するためのものです。
- これはコメントであり、
-
if(typ->kind&KindNoPointers)
ブロック内のflag
設定の変更:- 変更前:
if(typ->kind&KindNoPointers)
の条件が真の場合(つまり、スライスの要素がポインタを含まない型の場合)、flag |= FlagNoScan;
が実行され、既存のFlagNoZero
に加えてFlagNoScan
が設定されていました。 - 変更後:
if(typ->kind&KindNoPointers)
の条件が真の場合、flag = FlagNoScan|FlagNoZero;
が実行されます。これにより、ポインタを含まない型の場合にのみ、FlagNoScan
とFlagNoZero
の両方が明示的に設定されます。この場合、GCはそのメモリをスキャンせず、ゼロクリアも不要です。
- 変更前:
-
m->locks
の操作の削除:- 変更前:
runtime.mallocgc
の呼び出しの前後で、m->locks++
とm->locks--
が行われていました。これにより、メモリ割り当て中はプリエンプションが抑制されていました。 - 変更後: これらの
m->locks
の操作が完全に削除されました。これにより、growslice1
内のメモリ割り当て中もGCがプリエンプションをかけることが可能になり、メモリ使用量が増加した際にGCが適切にトリガーされるようになります。
- 変更前:
-
runtime.memclr
の条件付き実行への変更:- 変更前:
runtime.memclr(ret->array+lenmem, capmem-lenmem);
は常に実行され、新しく割り当てられたスライスバッキング配列の未使用部分をゼロクリアしていました。 - 変更後:
if(typ->kind&KindNoPointers)
の条件が真の場合にのみruntime.memclr
が実行されるようになりました。これは、ポインタを含まない型の場合、FlagNoZero
が設定されているため、明示的なゼロクリアが必要になるためです。ポインタを含む型の場合、runtime.mallocgc
がデフォルトでゼロクリアされたメモリを返すため、runtime.memclr
は不要になります。
- 変更前:
これらの変更により、growslice1
におけるメモリ割り当てがGCのプリエンプションを妨げなくなり、同時にGCの安全性が維持されるようになりました。特に、append
操作が頻繁に行われるシナリオでのメモリ枯渇問題が解決されます。
関連リンク
- Go言語の公式Issueトラッカー: https://github.com/golang/go/issues
- Go言語のソースコード: https://github.com/golang/go
参考にした情報源リンク
- コミットメッセージ:
8afa086ce67b44abb9c9639efca214db7acf7b3f
- Go言語のランタイムに関する一般的な知識
- Go言語のガベージコレクションに関する一般的な知識
- Go言語の
slice
の内部実装に関する一般的な知識 - Go言語の
m->locks
およびプリエンプションに関する一般的な知識 - Go言語の
runtime.mallocgc
および関連フラグに関する一般的な知識 - Go言語の
runtime.memmove
およびruntime.memclr
に関する一般的な知識 - Go言語のIssue #7922 (コミットメッセージに記載されているが、直接的なWeb検索では見つからなかったため、コミットメッセージの内容から推測)