1. ホーム
  2. スクリプト・コラム
  3. ゴラン

囲碁がGC問題の引き金になる場合の話

2022-02-15 17:08:16

初期の頃によく蔑まれたのが、ガベージコレクション(以後:GC)の仕組みでSTW(Stop-The-World)時間が長いことでした。そこで、現時点では、STWの開始として、Go言語がGCを発動するのはいつなのか?

1. GCとは何ですか?

コンピュータサイエンスにおいて、ガベージコレクション(GC)は、プログラムで使われなくなったオブジェクトとそれが占めるメモリをガベージコレクタが回収しようとする自動メモリ管理のメカニズムである。

最も古い John McCarthy を簡略化するために、1959年頃にガベージコレクションが発明されました。 Lisp という、手動でメモリを管理する仕組みがあります(@wikipediaより)。

2. なぜGCなのか

手動でメモリを管理するのは面倒ですし、メモリの管理を誤ったり足りなくなったりすると、プログラムが不安定になったり(リークが続いたり)、あからさまにクラッシュしたりすることに直結します。

3. GCトリガーシナリオ

GCトリガーのシナリオは、以下の2つに大別されます。

  • システムトリガー ランタイム自身が内蔵の条件に基づいてチェック、検出、そしてGCを行い、アプリケーション全体の可用性を維持します。
  • 手動によるトリガー : 開発者がビジネスコード自体で呼び出す runtime.GC メソッドを使用して GC 動作をトリガーします。

3.1 システムトリガー

システムトリガーシナリオでは、Goソースコードの src/runtime/mgc.go ファイルを作成します。 は、GCのシステムトリガーについて、以下の3つのシナリオを明示的に特定しています。

const ( 
 gcTriggerHeap gcTriggerKind = iota 
 gcTriggerTime 
 gcTriggerCycle 
) 




  • gcTriggerHeap. 割り当てられたヒープサイズが閾値(コントローラで計算されたトリガーヒープのサイズ)に達するとトリガーされます。
  • gcTriggerTime. 最後のGCサイクルから一定時間が経過したときにトリガーされる。時間サイクルの始まりは runtime.forcegcperiod 変数で指定され、デフォルトは2分です。
  • gcTriggerCycle. GCがオンになっていない場合、GCを開始します。

手動でトリガーされた runtime.GC メソッドが関与しています。

3.2 手動トリガー

手動トリガーのシナリオでは、唯一のGo言語である runtime.GC メソッドをトリガーすることができ、分類するための余分なものは何もありません。

しかし、どのようなビジネスシナリオが、通常、GCに手動で干渉し、強制的に起動させることになるのか、考えなければなりません。

手動で強制的にトリガーをかける必要があるシナリオは、極めて稀です。あるビジネスメソッドが実行された後、メモリを使いすぎたために人為的に解放する必要がある場合です。あるいは debug がプログラムによって要求される。

3.3 基本的な流れ

GoがGCをトリガーするシナリオを理解した後、GCをトリガーするフローコードがどのように見えるか、手動でトリガーされる runtime.GC メソッドを突破口にします。

コアとなるコードは以下の通りです。

func GC() { 
 n := atomic.Load(&work.cycles) 
 gcWaitOnMark(n) 
 
 gcStart(gcTrigger{kind: gcTriggerCycle, n: n + 1}) 
   
 gcWaitOnMark(n + 1) 
 
 for atomic.Load(&work.cycles) == n+1 && sweepone() ! = ^uintptr(0) { 
  sweep.nbgsweep++ 
  Gosched() 
 } 
   
 for atomic.Load(&work.cycles) == n+1 && atomic.Load(&mheap_.sweepers) ! = 0 { 
  Gosched() 
 } 
   
 mp := acquirem() 
 cycle := atomic.Load(&work.cycles) 
 if cycle == n+1 || (gcphase == _GCmark && cycle == n+2) { 
  mProf_PostSweep() 
 } 
 releasem(mp) 
} 



新しいGCサイクルを開始する前に gcWaitOnMark メソッドを使用して、前のGCラウンドの終了をマークします(スキャン終了、マーク、マーク終了などによる)。

を呼び出して新しいGCサイクルを開始します。 gcStart メソッドはGCの動作をトリガーし、スキャンマーカーフェーズを開始します。

を呼び出す必要があります。 gcWaitOnMark メソッドを使用して、現在のGCサイクルのスキャン、マーク、マーク終了が完了するまで待ちます。

を呼び出す必要があります。 sweepone メソッドを使用して、掃引されていないヒープスパンをスキャンし、クリーンアップが完了するように掃引を続けます。掃引が完了するのを待つまでのブロック時間中に Gosched を出すようにします。

mProf_PostSweepメソッドは、GCの現在のラウンドがほぼ完了した後に呼び出されます。これは、最後のマーカー終了時のヒーププロファイルのスナップショットを記録します。

Mの解放で終了。

3.4 トリガーとなる場所

GCの基本的な流れを見て、基本的な理解はできました。でも、もしかしたら、また疑問に思った方もいらっしゃるかもしれませんね?

今回のタイトルは "GCはいつ発動するのか"ですが、先に発動するタイミングは分かっています。しかし......Goはトリガーする仕組みをどこに実装しているのでしょうか、フローから完全に抜け落ちているように思えますが......。

4. スレッドの監視

要するに、Goランタイムでは (runtime) が初期化されると goroutine GC機構に関連することを処理する。

そのコードは以下の通りです。

func init() { 
 go forcegchelper() 
} 
 
func forcegchelper() { 
 forcegc.g = getg() 
 lockInit(&forcegc.lock, lockRankForcegc) 
 for { 
  lock(&forcegc.lock) 
  if forcegc.idle ! = 0 { 
   throw("forcegc: phase error") 
  } 
  atomic.Store(&forcegc.idle, 1) 
  goparkunlock(&forcegc.lock, waitReasonForceGCIdle, traceEvGoBlock, 1) 
    // this goroutine is explicitly resumed by sysmon 
  if debug.gctrace > 0 { 
   println("GC forced") 
  } 
 
  gcStart(gcTrigger{kind: gcTriggerTime, now: nanotime()}) 
 } 
} 



このプログラムにおいて、特に気になるのは forcegchelper メソッドを呼び出します。 goparkunlock メソッドで goroutine を休眠待機状態にし、不要なリソースのオーバーヘッドを削減します。

ハイバネーション後 sysmon これは、監視や目覚ましなどの動作を行うシステム監視スレッドである

func sysmon() { 
 ... 
 for { 
  ... 
  // check if we need to force a GC 
  if t := (gcTrigger{kind: gcTriggerTime, now: now}); t.test() && atomic.Load(&forcegc.idle) ! = 0 { 
   lock(&forcegc.lock) 
   forcegc.idle = 0 
   var list gList 
   list.push(forcegc.g) 
   injectglist(&list) 
   unlock(&forcegc.lock) 
  } 
  if debug.schedtrace > 0 && lasttrace+int64(debug.schedtrace)*1000000 <= now { 
   lasttrace = now 
   schedtrace(debug.scheddetail > 0) 
  } 
  unlock(&sched.sysmonlock) 
 } 
} 



このコードの核となる動作は、forループを繰り返し、その中で gcTriggerTime now 変数を使用して、一定の時間に達したかどうかを判断します(デフォルトは2分)。

reachedが条件を満たしたことを意味する場合 forcegc.g をグローバルキューに入れ、新しいラウンドのスケジューリングを受け、上記の forcegchelper 上記

5. ヒープメモリの要求

タイミングトリガーの仕組みを理解した上で、もう一つのシナリオはヒープ領域が確保される時なので、どこを見るかはかなり明確です。

これは、ヒープメモリの実行時要求である mallocgc メソッドを使用します。コアとなるコードは以下の通りです。

func mallocgc(size uintptr, typ *_type, needzero bool) unsafe.Pointer { 
 shouldhelpgc := false 
 ... 
 if size <= maxSmallSize { 
  if noscan && size < maxTinySize { 
   ... 
   // Allocate a new maxTinySize block. 
   span = c.alloc[tinySpanClass] 
   v := nextFreeFast(span) 
   if v == 0 { 
    v, span, shouldhelpgc = c.nextFree(tinySpanClass) 
   } 
   ... 
   spc := makeSpanClass(sizeclass, noscan) 
   span = c.alloc[spc] 
   v := nextFreeFast(span) 
   if v == 0 { 
    v, span, shouldhelpgc = c.nextFree(spc) 
   } 
   ... 
  } 
 } else { 
  shouldhelpgc = true 
  span = c.allocLarge(size, needzero, noscan) 
  ... 
 } 
 
 if shouldhelpgc { 
  if t := (gcTrigger{kind: gcTriggerHeap}); t.test() { 
   gcStart(t) 
  } 
 } 
 
 return x 
} 




小型オブジェクト : スモールオブジェクトを要求したときに、メモリに空きスパンがないことがわかったら nextFree メソッドで新しい利用可能なオブジェクトを取得する必要があり、GC 動作を引き起こす可能性があります。

大きなオブジェクト 32kを超えるラージオブジェクトが要求された場合、GC動作が発生する場合があります。

まとめ
今回は、Go言語をトリガーとしたGCのシナリオを大きく2つに分類して紹介し、それぞれを大分類の中のシナリオのサブセットを基に説明しました。

GoがGCを誘発する場合についての話はこれで終わりです。GoがGCを誘発する場合についての詳しい情報は、Scripting Houseの過去の記事を検索するか、以下の記事を引き続き閲覧してください。