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

[インデックス 17165] ファイルの概要

このコミットは、Goランタイムにおけるデータ競合検出器(Race Detector)に関連する未使用のフィールド m->racepc を削除するものです。この変更により、コードの簡素化と不要なコードパスの削除が行われました。

コミット

commit fa4628346b78f8847f11166d52d250abb6cb50df
Author: Dmitriy Vyukov <dvyukov@google.com>
Date:   Mon Aug 12 21:48:19 2013 +0400

    runtime: remove unused m->racepc
    The original plan was to collect allocation stacks
    for all memory blocks. But it was never implemented
    and it's not in near plans and it's unclear how to do it at all.
    
    R=golang-dev, dave, bradfitz
    CC=golang-dev
    https://golang.org/cl/12724044

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/fa4628346b78f8847f11166d52d250abb6cb50df

元コミット内容

Goランタイムから未使用の m->racepc フィールドを削除します。このフィールドは、元々すべてのメモリブロックに対するアロケーションスタックを収集するために計画されていましたが、実装されることはなく、将来的な計画も不明確であったため、削除されました。

変更の背景

Goのデータ競合検出器(Race Detector)は、並行処理におけるデータ競合(複数のゴルーチンが同時に同じメモリ位置にアクセスし、少なくとも一方が書き込み操作である場合に発生する競合)を検出するためのツールです。このツールは、プログラムの実行時にメモリアクセスを監視し、競合の可能性を報告します。

m->racepc フィールドは、Goランタイムの M 構造体(OSスレッドを表す)に存在していました。コミットメッセージによると、このフィールドは「すべてメモリブロックに対するアロケーションスタックを収集する」という目的で導入が検討されていました。アロケーションスタックとは、特定のメモリがどこで、どのようなコールスタックによって割り当てられたかを示す情報です。データ競合のデバッグにおいて、競合が発生したメモリがどこで割り当てられたかを知ることは非常に有用です。

しかし、この機能は実際に実装されることはなく、将来的な実装計画も不透明でした。未使用のコードやデータ構造は、コードベースの複雑性を増し、メンテナンスを困難にし、潜在的なバグの原因となる可能性があります。そのため、Dmitriy Vyukov氏(Goランタイムの主要な貢献者の一人であり、特にデータ競合検出器の開発に深く関わっています)によって、この未使用の m->racepc フィールドを削除する決定がなされました。これは、コードベースの健全性を保ち、不要なオーバーヘッドを削減するためのクリーンアップ作業の一環です。

前提知識の解説

GoランタイムのM、G、Pモデル

Goランタイムは、ゴルーチン(G)、論理プロセッサ(P)、OSスレッド(M)という3つの主要な抽象化を用いて並行処理を管理します。

  • G (Goroutine): Goにおける軽量な実行単位です。数千から数百万のゴルーチンを同時に実行できます。
  • P (Processor): ゴルーチンを実行するための論理的なCPUコアです。Goスケジューラは、Pにゴルーチンを割り当てて実行します。Pの数は通常、CPUのコア数に設定されます。
  • M (Machine/OS Thread): 実際のOSスレッドです。Pに割り当てられたゴルーチンは、M上で実行されます。MはOSのスケジューラによって管理されます。

m->racepcm は、この M 構造体を指しており、特定のOSスレッドに関連付けられたデータであることを示唆しています。

Goデータ競合検出器 (Go Race Detector)

Goのデータ競合検出器は、Go 1.1で導入された強力なツールです。これは、プログラムの実行中にメモリアクセスを監視し、複数のゴルーチンが同時に同じメモリ位置にアクセスし、少なくとも一方が書き込み操作である場合に発生するデータ競合を検出します。データ競合は、プログラムの予測不能な動作やクラッシュの原因となることがあり、デバッグが非常に困難です。

データ競合検出器は、通常、コンパイル時に -race フラグを付けてビルドすることで有効になります(例: go build -race)。有効にすると、ランタイムが追加のインストゥルメンテーションコードを挿入し、メモリの読み書き操作を追跡します。競合が検出されると、検出器は競合が発生した場所のスタックトレースを含む詳細なレポートを出力します。

アロケーションスタック

アロケーションスタックとは、特定のメモリ領域がプログラムのどの部分で、どのような関数呼び出しの連鎖(スタックトレース)を経て割り当てられたかを示す情報です。データ競合のデバッグにおいて、競合が発生したメモリがどこで割り当てられたかを知ることは、問題の根本原因を特定する上で非常に役立ちます。例えば、あるデータ構造が不適切な場所で共有され、複数のゴルーチンから同時にアクセスされている場合、そのデータ構造がどこで生成されたかを知ることで、設計上の問題を特定しやすくなります。

技術的詳細

このコミットの技術的な核心は、Goランタイムの内部構造、特にデータ競合検出器の動作とメモリ管理に関連しています。

m->racepc は、runtime.M 構造体(OSスレッドを表す)のメンバーでした。racepcpc は "Program Counter"(プログラムカウンタ)の略であり、通常は命令ポインタ、つまり実行中の命令のアドレスを指します。この文脈では、メモリ割り当てが行われた時点のプログラムカウンタ、つまり呼び出し元の命令のアドレスを記録しようとしていたと考えられます。

コミットの差分を見ると、m->racepc は主に runtime·mallocgcruntime·new というメモリ割り当てに関連する関数で使用されていました。

  • runtime·mallocgc: Goのガベージコレクタが管理するヒープからメモリを割り当てる主要な関数です。
  • runtime·new: Goの new キーワードに対応するランタイム関数で、ゼロ値で初期化された新しい型のインスタンスを割り当てます。

元のコードでは、runtime·new が呼び出される際に m->racepcruntime·getcallerpc(&typ) の結果を格納していました。これは、new が呼び出された場所のプログラムカウンタを取得し、それを現在のM(OSスレッド)に関連付けられた racepc フィールドに保存しようとしていたことを示唆しています。その後、runtime·mallocgcracemalloc を呼び出す際に、この m->racepc の値を pc 引数として渡していました。

しかし、コミットメッセージにあるように、この pc 引数(アロケーションスタックを伝えるためのもの)は racemalloc の内部で実際に利用されることはありませんでした。racemalloc は、割り当てられたメモリブロック p とそのサイズ sz をデータ競合検出器のコアロジックである runtime∕race·Malloc に渡しますが、pc の値は /* unused pc */ 0 としてハードコードされて渡されるか、あるいは race0.c のスタブ実装では単に USED(pc) とマークされるだけで、実際の処理には使われていませんでした。

このコミットは、この未使用の m->racepc フィールドと、それに関連するコードパスを完全に削除することで、以下の効果をもたらします。

  1. コードの簡素化: 不要なフィールドとそれに関連する割り当て/伝播ロジックが削除され、コードベースがよりクリーンになります。
  2. メモリ使用量の削減: M 構造体から racepc フィールドが削除されることで、各OSスレッドが消費するメモリ量がわずかながら削減されます。これは、多数のMが存在するような極端なシナリオでは、全体的なメモリフットプリントに影響を与える可能性があります。
  3. パフォーマンスの向上(微小): m->racepc への値の書き込みや読み出しといった、不要な操作がなくなるため、ごくわずかですがパフォーマンスが向上する可能性があります。特に、メモリ割り当てが頻繁に行われるようなワークロードでは、その影響が累積する可能性も考えられます。
  4. 誤解の排除: 未使用のフィールドが残っていると、将来の開発者がその目的を誤解したり、誤って利用しようとしたりする可能性があります。削除することで、このような混乱を避けることができます。

この変更は、Goランタイムの成熟と継続的な改善プロセスの一例であり、未使用の機能やコードを定期的にクリーンアップすることの重要性を示しています。

コアとなるコードの変更箇所

このコミットでは、主に以下のファイルが変更されています。

  1. src/pkg/runtime/malloc.goc:

    • runtime·mallocgc 関数内で、raceenabled の場合に runtime·racemalloc を呼び出す際、m->racepc を引数として渡す部分が削除され、runtime·racemalloc(v, size) と引数が減らされています。
    • runtime·new 関数内で、raceenabled の場合に m->racepc = runtime·getcallerpc(&typ); を設定する行が削除されています。
  2. src/pkg/runtime/race.c:

    • runtime·racemalloc 関数のシグネチャが void runtime·racemalloc(void *p, uintptr sz, void *pc) から void runtime·racemalloc(void *p, uintptr sz) に変更され、pc 引数が削除されています。
    • 内部の runtime∕race·Malloc の呼び出しで、pc の代わりに /* unused pc */ 0 が渡されるよう変更されています。
  3. src/pkg/runtime/race.h:

    • runtime·racemalloc 関数のプロトタイプ宣言が、pc 引数なしの形式に更新されています。
  4. src/pkg/runtime/race0.c:

    • runtime·racemalloc 関数のスタブ実装のシグネチャが変更され、pc 引数が削除されています。
  5. src/pkg/runtime/runtime.h:

    • struct M 構造体から void* racepc; の定義が削除されています。

コアとなるコードの解説

src/pkg/runtime/malloc.goc の変更

--- a/src/pkg/runtime/malloc.goc
+++ b/src/pkg/runtime/malloc.goc
@@ -141,10 +141,8 @@ runtime·mallocgc(uintptr size, uintptr typ, uint32 flag)
  	if(!(flag & FlagNoInvokeGC) && mstats.heap_alloc >= mstats.next_gc)
  		runtime·gc(0);
 
-	if(raceenabled) {
-		runtime·racemalloc(v, size, m->racepc);
-		m->racepc = nil;
-	}
+	if(raceenabled)
+		runtime·racemalloc(v, size);
  	return v;
  }
 
@@ -702,8 +700,6 @@ runtime·mal(uintptr n)
  void
  runtime·new(Type *typ, uint8 *ret)
  {
-	if(raceenabled)
-		m->racepc = runtime·getcallerpc(&typ);
  	ret = runtime·mallocgc(typ->size, (uintptr)typ | TypeInfo_SingleObject, typ->kind&KindNoPointers ? FlagNoPointers : 0);
  	FLUSH(&ret);
  }
  • runtime·mallocgc: メモリ割り当ての主要なパスです。以前は raceenabled(データ競合検出器が有効な場合)に m->racepcruntime·racemalloc に渡していました。この変更により、m->racepc の設定と引き渡しが完全に削除され、runtime·racemallocv(割り当てられたメモリのアドレス)と size のみを受け取るようになりました。
  • runtime·new: new キーワードによる型インスタンスの割り当てを処理します。以前は、ここで runtime·getcallerpc(&typ) を呼び出して、new が呼び出された場所のプログラムカウンタを m->racepc に保存していました。この行が削除されたことで、m->racepc に値を設定する処理がなくなりました。

src/pkg/runtime/race.c の変更

--- a/src/pkg/runtime/race.c
+++ b/src/pkg/runtime/race.c
@@ -138,13 +138,13 @@ runtime·racefuncexit(void)
 }
 
 void
-runtime·racemalloc(void *p, uintptr sz, void *pc)
+runtime·racemalloc(void *p, uintptr sz)
 {
 	// use m->curg because runtime·stackalloc() is called from g0
 	if(m->curg == nil)
 		return;
 	m->racecall = true;
-	runtime∕race·Malloc(m->curg->racectx, p, sz, pc);
+	runtime∕race·Malloc(m->curg->racectx, p, sz, /* unused pc */ 0);
 	m->racecall = false;
 }
  • runtime·racemalloc: データ競合検出器がメモリ割り当てを記録するために呼び出す関数です。この関数のシグネチャから void *pc 引数が削除されました。内部では、runtime∕race·Malloc という実際の競合検出ロジックを呼び出していますが、以前 pc を渡していた箇所が /* unused pc */ 0 というハードコードされた値に置き換えられています。これは、pc が実際に利用されていなかったことを明確に示しています。

src/pkg/runtime/race.h の変更

--- a/src/pkg/runtime/race.h
+++ b/src/pkg/runtime/race.h
@@ -16,7 +16,7 @@ uintptr	runtime·raceinit(void);
 void	runtime·racefini(void);
 
 void	runtime·racemapshadow(void *addr, uintptr size);
-void	runtime·racemalloc(void *p, uintptr sz, void *pc);
+void	runtime·racemalloc(void *p, uintptr sz);
 void	runtime·racefree(void *p);
 uintptr	runtime·racegostart(void *pc);
 void	runtime·racegoend(void);
  • runtime·racemalloc のプロトタイプ宣言: race.c での変更に合わせて、関数のプロトタイプ宣言からも pc 引数が削除されています。これにより、コンパイラが新しいシグネチャを認識し、整合性が保たれます。

src/pkg/runtime/race0.c の変更

--- a/src/pkg/runtime/race0.c
+++ b/src/pkg/runtime/race0.c
@@ -105,11 +105,10 @@ runtime·racefingo(void)
 }
 
 void
-runtime·racemalloc(void *p, uintptr sz, void *pc)
+runtime·racemalloc(void *p, uintptr sz)
 {
 	USED(p);
 	USED(sz);
-\tUSED(pc);
 }
  • runtime·racemalloc のスタブ実装: race0.c は、データ競合検出器が無効な場合のスタブ実装を提供します。ここでも pc 引数が削除され、USED(pc) という未使用変数警告を抑制するためのマクロも不要になったため削除されています。

src/pkg/runtime/runtime.h の変更

--- a/src/pkg/runtime/runtime.h
+++ b/src/pkg/runtime/runtime.h
@@ -339,7 +339,6 @@ struct	M
 	GCStats	gcstats;
 	bool	racecall;
 	bool	needextram;
-\tvoid*	racepc;
 	void	(*waitunlockf)(Lock*);
 	void*	waitlock;
 
  • struct M: GoランタイムのOSスレッドを表す M 構造体から、void* racepc; の定義が削除されています。これにより、M 構造体のサイズがわずかに減少し、メモリフットプリントが最適化されます。

これらの変更は、m->racepc フィールドがGoランタイムのデータ競合検出器の設計において、当初意図された役割を果たしていなかったことを明確にし、コードベースからその痕跡を完全に削除することで、ランタイムの効率性と保守性を向上させています。

関連リンク

参考にした情報源リンク