Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

[インデックス 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 を設定しつつ、FlagNoScantyp->kind&KindNoPointers(要素がポインタを含まない型であるか)に依存していました。そして、FlagNoScan が設定されていない場合(つまり、ポインタを含む可能性のある要素型の場合)、runtime.memclr を使って割り当てられたメモリをゼロクリアしていました。このゼロクリアは、GCがそのメモリをスキャンする前に、古いガベージポインタを消去するために重要でした。

m->locks の保護下で runtime.mallocgc が呼び出され、その後 m->locks の保護下で runtime.memclr が呼び出されるというシーケンスは、GCが不完全な状態のメモリをスキャンするのを防ぐためのものでした。しかし、この保護がGCのトリガーを妨げるという副作用があったため、変更が必要となりました。

変更の意図

このコミットの主な意図は、growslice1 におけるメモリ割り当てがGCのプリエンプションを妨げないようにすることです。m->locks の操作を削除することで、growslice1 がメモリを割り当てている最中でもGCがプリエンプションをかけ、GCサイクルを開始できるようになります。

FlagNoZeroFlagNoScan の組み合わせの変更は、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

コアとなるコードの解説

変更点を詳細に見ていきます。

  1. flag = FlagNoZero; の削除と flag = 0; への変更:

    • 変更前: runtime.mallocgc に渡す flag の初期値として FlagNoZero が設定されていました。これは、割り当てられたメモリをゼロクリアしないことを意味します。
    • 変更後: flag の初期値が 0 になりました。これは、runtime.mallocgc がデフォルトでメモリをゼロクリアすることを意味します。これにより、ポインタを含む可能性のある型の場合でも、割り当てられたメモリが常にクリーンな状態であることが保証され、GCが古いガベージポインタをスキャンするリスクがなくなります。
  2. // Can't use FlagNoZero w/o FlagNoScan, because otherwise GC can scan unitialized memory. の追加:

    • これはコメントであり、FlagNoZeroFlagNoScan なしで使用することの危険性について説明しています。つまり、メモリがゼロクリアされず(FlagNoZero)、かつGCがそのメモリをスキャンする(FlagNoScan なし)場合、GCは初期化されていない(古いガベージを含む)メモリをスキャンしてしまう可能性がある、という警告です。このコミットの変更は、この問題を回避するためのものです。
  3. if(typ->kind&KindNoPointers) ブロック内の flag 設定の変更:

    • 変更前: if(typ->kind&KindNoPointers) の条件が真の場合(つまり、スライスの要素がポインタを含まない型の場合)、flag |= FlagNoScan; が実行され、既存の FlagNoZero に加えて FlagNoScan が設定されていました。
    • 変更後: if(typ->kind&KindNoPointers) の条件が真の場合、flag = FlagNoScan|FlagNoZero; が実行されます。これにより、ポインタを含まない型の場合にのみ、FlagNoScanFlagNoZero の両方が明示的に設定されます。この場合、GCはそのメモリをスキャンせず、ゼロクリアも不要です。
  4. m->locks の操作の削除:

    • 変更前: runtime.mallocgc の呼び出しの前後で、m->locks++m->locks-- が行われていました。これにより、メモリ割り当て中はプリエンプションが抑制されていました。
    • 変更後: これらの m->locks の操作が完全に削除されました。これにより、growslice1 内のメモリ割り当て中もGCがプリエンプションをかけることが可能になり、メモリ使用量が増加した際にGCが適切にトリガーされるようになります。
  5. runtime.memclr の条件付き実行への変更:

    • 変更前: runtime.memclr(ret->array+lenmem, capmem-lenmem); は常に実行され、新しく割り当てられたスライスバッキング配列の未使用部分をゼロクリアしていました。
    • 変更後: if(typ->kind&KindNoPointers) の条件が真の場合にのみ runtime.memclr が実行されるようになりました。これは、ポインタを含まない型の場合、FlagNoZero が設定されているため、明示的なゼロクリアが必要になるためです。ポインタを含む型の場合、runtime.mallocgc がデフォルトでゼロクリアされたメモリを返すため、runtime.memclr は不要になります。

これらの変更により、growslice1 におけるメモリ割り当てがGCのプリエンプションを妨げなくなり、同時にGCの安全性が維持されるようになりました。特に、append 操作が頻繁に行われるシナリオでのメモリ枯渇問題が解決されます。

関連リンク

参考にした情報源リンク

  • コミットメッセージ: 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, and mallocgc 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

コアとなるコードの解説

変更点を詳細に見ていきます。

  1. flag = FlagNoZero; の削除と flag = 0; への変更:

    • 変更前: runtime.mallocgc に渡す flag の初期値として FlagNoZero が設定されていました。これは、割り当てられたメモリをゼロクリアしないことを意味します。
    • 変更後: flag の初期値が 0 になりました。これは、runtime.mallocgc がデフォルトでメモリをゼロクリアすることを意味します。これにより、ポインタを含む可能性のある型の場合でも、割り当てられたメモリが常にクリーンな状態であることが保証され、GCが古いガベージポインタをスキャンするリスクがなくなります。
  2. // Can't use FlagNoZero w/o FlagNoScan, because otherwise GC can scan unitialized memory. の追加:

    • これはコメントであり、FlagNoZeroFlagNoScan なしで使用することの危険性について説明しています。つまり、メモリがゼロクリアされず(FlagNoZero)、かつGCがそのメモリをスキャンする(FlagNoScan なし)場合、GCは初期化されていない(古いガベージを含む)メモリをスキャンしてしまう可能性がある、という警告です。このコミットの変更は、この問題を回避するためのものです。
  3. if(typ->kind&KindNoPointers) ブロック内の flag 設定の変更:

    • 変更前: if(typ->kind&KindNoPointers) の条件が真の場合(つまり、スライスの要素がポインタを含まない型の場合)、flag |= FlagNoScan; が実行され、既存の FlagNoZero に加えて FlagNoScan が設定されていました。
    • 変更後: if(typ->kind&KindNoPointers) の条件が真の場合、flag = FlagNoScan|FlagNoZero; が実行されます。これにより、ポインタを含まない型の場合にのみ、FlagNoScanFlagNoZero の両方が明示的に設定されます。この場合、GCはそのメモリをスキャンせず、ゼロクリアも不要です。
  4. m->locks の操作の削除:

    • 変更前: runtime.mallocgc の呼び出しの前後で、m->locks++m->locks-- が行われていました。これにより、メモリ割り当て中はプリエンプションが抑制されていました。
    • 変更後: これらの m->locks の操作が完全に削除されました。これにより、growslice1 内のメモリ割り当て中もGCがプリエンプションをかけることが可能になり、メモリ使用量が増加した際にGCが適切にトリガーされるようになります。
  5. runtime.memclr の条件付き実行への変更:

    • 変更前: runtime.memclr(ret->array+lenmem, capmem-lenmem); は常に実行され、新しく割り当てられたスライスバッキング配列の未使用部分をゼロクリアしていました。
    • 変更後: if(typ->kind&KindNoPointers) の条件が真の場合にのみ runtime.memclr が実行されるようになりました。これは、ポインタを含まない型の場合、FlagNoZero が設定されているため、明示的なゼロクリアが必要になるためです。ポインタを含む型の場合、runtime.mallocgc がデフォルトでゼロクリアされたメモリを返すため、runtime.memclr は不要になります。

これらの変更により、growslice1 におけるメモリ割り当てがGCのプリエンプションを妨げなくなり、同時にGCの安全性が維持されるようになりました。特に、append 操作が頻繁に行われるシナリオでのメモリ枯渇問題が解決されます。

関連リンク

参考にした情報源リンク

  • コミットメッセージ: 8afa086ce67b44abb9c9639efca214db7acf7b3f
  • Go言語のランタイムに関する一般的な知識
  • Go言語のガベージコレクションに関する一般的な知識
  • Go言語の slice の内部実装に関する一般的な知識
  • Go言語の m->locks およびプリエンプションに関する一般的な知識
  • Go言語の runtime.mallocgc および関連フラグに関する一般的な知識
  • Go言語の runtime.memmove および runtime.memclr に関する一般的な知識
  • Go言語のIssue #7922 (コミットメッセージに記載されているが、直接的なWeb検索では見つからなかったため、コミットメッセージの内容から推測)