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

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

このコミットは、GoランタイムにおけるNote型をunionからstructに変更するものです。この変更の主な目的は、Goの正確なガベージコレクション(Precise GC)がunion型によって妨げられる問題を解決することにあります。

コミット

commit 58030c541bd39b287aa66569e58094279b7cf642
Author: Dmitriy Vyukov <dvyukov@google.com>
Date:   Sat Apr 6 20:09:02 2013 -0700

    runtime: change Note from union to struct
    Unions can break precise GC.
    Update #5193.
    
    R=golang-dev, iant
    CC=golang-dev
    https://golang.org/cl/8362046

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

https://github.com/golang/go/commit/58030c541bd39b287aa66569e58094279b7cf642

元コミット内容

このコミットは、Goランタイムの同期プリミティブで使用されるNote型を、unionからstructに変更します。これにより、unionが正確なガベージコレクション(Precise GC)を妨げる可能性のある問題を解消します。関連する問題として、Go issue #5193が挙げられています。

変更の背景

Go言語のランタイムは、効率的なメモリ管理のためにガベージコレクション(GC)を使用しています。特に、Goは「正確なGC(Precise GC)」を採用しており、これはメモリ上のどの値がポインタであるかを正確に識別し、到達可能なオブジェクトのみを保持することを意味します。これにより、メモリリークを防ぎ、プログラムの安定性を高めます。

しかし、C言語で実装されているGoランタイムの一部では、union型が使用されていました。unionは、複数の異なる型のメンバーが同じメモリ領域を共有することを可能にするデータ構造です。例えば、Note型は、uint32(符号なし32ビット整数)として扱われるkeyと、M*M構造体へのポインタ)として扱われるwaitmを共有していました。

このunionの性質が、正確なGCにとって問題となります。GCはメモリをスキャンしてポインタを識別しますが、unionの場合、あるメモリ位置が現在整数として使用されているのか、それともポインタとして使用されているのかをGCが確実に判断することが困難になります。もしGCが整数値を誤ってポインタと解釈したり、逆にポインタを整数と誤解したりすると、以下のような深刻な問題が発生する可能性があります。

  1. メモリリーク: GCがポインタを認識できず、到達可能なオブジェクトが誤って解放されずにメモリ上に残り続ける。
  2. クラッシュやデータ破損: GCが非ポインタ値をポインタと誤解し、その値を逆参照しようとすることで、不正なメモリアクセスが発生し、プログラムがクラッシュしたりデータが破損したりする。

この問題は、Go issue #5193 (runtime: unions break precise GC) で報告されており、このコミットはその解決策として提案されました。

前提知識の解説

ガベージコレクション (GC)

ガベージコレクションは、プログラムが動的に確保したメモリ領域のうち、もはや使用されていない(到達不可能になった)ものを自動的に解放する仕組みです。これにより、プログラマは手動でのメモリ管理の煩雑さから解放され、メモリリークのリスクを低減できます。

  • 正確なGC (Precise GC): メモリ上のどの値がポインタであるかを正確に識別し、ポインタが指すオブジェクトを追跡することで、到達可能なオブジェクトのみを保持します。GoのGCは正確なGCです。
  • 保守的なGC (Conservative GC): メモリ上の値がポインタであるかどうかを確実に判断できない場合でも、それがポインタである可能性があると仮定して、その値を追跡します。これにより、メモリリークは発生しにくいですが、不要なオブジェクトが解放されない「偽のリーク」が発生する可能性があります。

C言語のunionstruct

  • struct (構造体): 複数の異なる型のメンバーをまとめて一つのデータ型として扱います。各メンバーはそれぞれ異なるメモリ領域を占有します。
  • union (共用体): 複数の異なる型のメンバーが同じメモリ領域を共有します。一度にアクティブになるメンバーは一つだけであり、そのメンバーの型に応じてメモリが解釈されます。unionのサイズは、その中で最も大きいメンバーのサイズによって決まります。

Goランタイムの同期プリミティブ

Goランタイムは、ゴルーチン間の協調や排他制御のために、様々な同期プリミティブを提供しています。このコミットで変更されるNote型は、これらの同期プリミティブ(例えば、sync.Mutexsync.WaitGroupの内部実装)の基盤となる低レベルなメカニズム、特にfutex(Fast Userspace muTexes)やセマフォベースの待機/通知メカニズムで使用されます。

  • Futex: Linuxカーネルが提供する軽量な同期プリミティブで、ユーザー空間でのロック競合が少ない場合にカーネルへのシステムコールを回避し、効率的な同期を実現します。
  • セマフォ: 複数のプロセスやスレッドが共有リソースにアクセスする際の同期を制御するためのメカニズムです。

技術的詳細

このコミットの核心は、src/pkg/runtime/runtime.hにおけるNote型の定義変更です。

変更前 (union Note):

union Note
{
	uint32	key;	// futex-based impl
	M*	waitm;	// waiting M (sema-based impl)
};

この定義では、keyuint32)とwaitmM*、ポインタ)が同じメモリ領域を共有していました。GoのGCは、ポインタを正確に識別するために、メモリ上の各ワードがポインタであるか否かを判断する必要があります。unionの場合、同じメモリ領域が時には整数(uint32)として、時にはポインタ(M*)として使用されるため、GCがそのメモリ領域をスキャンする際に、どちらの型として解釈すべきか曖昧になります。これにより、GCが誤った判断を下し、前述のような問題を引き起こす可能性がありました。

変更後 (struct Note):

struct Note
{
	// Futex-based impl treats it as uint32 key,
	// while sema-based impl as M* waitm.
	// Used to be a union, but unions break precise GC.
	uintptr	key;
};

変更後、Notestructとなり、単一のメンバーkey(型はuintptr)を持つようになりました。 uintptrは、ポインタを保持するのに十分な大きさを持つ符号なし整数型です。この変更により、Note構造体のメモリレイアウトは常にuintptrという整数型として明確に定義されます。GCはuintptrをポインタではないと認識するため、このメモリ領域をスキャンする際にポインタとして誤解釈するリスクがなくなります。

実際のコード(lock_futex.clock_sema.c)では、このuintptr keyを、必要に応じてuint32*M*(C言語ではvoid**として扱われる)にキャストして使用します。これにより、C言語レベルでの型安全性を保ちつつ、GCにとっては曖昧さのないメモリレイアウトを提供します。

例えば、runtime·xchg(&n->key, 1)のような操作は、n->keyuintptr型であるため、runtime·xchg((uint32*)&n->key, 1)のように明示的にuint32*にキャストされます。これは、runtime·xchg関数がuint32*型の引数を期待しているためです。同様に、ポインタとして扱う必要がある箇所では、runtime·atomicloadp((void**)&n->key)のようにvoid**にキャストされます。

このアプローチにより、Goのランタイムは、GCの正確性を損なうことなく、低レベルな同期プリミティブの柔軟な実装を維持できます。

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

src/pkg/runtime/lock_futex.c

Note構造体のkeyメンバーへのアクセスにおいて、uint32*への明示的なキャストが追加されました。 例:

  • runtime·xchg(&n->key, 1) -> runtime·xchg((uint32*)&n->key, 1)
  • runtime·futexwakeup(&n->key, 1) -> runtime·futexwakeup((uint32*)&n->key, 1)
  • runtime·atomicload(&n->key) -> runtime·atomicload((uint32*)&n->key)
  • runtime·futexsleep(&n->key, 0, -1) -> runtime·futexsleep((uint32*)&n->key, 0, -1)

src/pkg/runtime/lock_sema.c

Note構造体のkeyメンバーへのアクセスにおいて、void**への明示的なキャストが追加されたり、n->waitmn->keyに置き換えられたりしました。 例:

  • n->waitm = nil; -> n->key = 0;
  • runtime·atomicloadp(&n->waitm) -> runtime·atomicloadp((void**)&n->key)
  • runtime·casp(&n->waitm, mp, (void*)LOCKED) -> runtime·casp((void**)&n->key, mp, (void*)LOCKED)
  • n->waitm != (void*)LOCKED -> n->key != LOCKED

src/pkg/runtime/runtime.h

Note型の定義がunionからstructに変更され、内部メンバーがuint32 key; M* waitm;から単一のuintptr key;に変更されました。

--- a/src/pkg/runtime/runtime.h
+++ b/src/pkg/runtime/runtime.h
@@ -54,7 +54,7 @@ typedef	struct	Lock		Lock;
 typedef	struct	M		M;
 typedef	struct	P		P;
 typedef	struct	Mem		Mem;
-typedef	union	Note		Note;
+typedef	struct	Note		Note;
 typedef	struct	Slice		Slice;
 typedef	struct	Stktop		Stktop;
 typedef	struct	String		String;
@@ -163,10 +163,12 @@ struct	Lock
 	// Used to be a union, but unions break precise GC.
 	uintptr	key;
 };
-union	Note
+struct	Note
 {
-\tuint32	key;	// futex-based impl
-\tM*	waitm;	// waiting M (sema-based impl)
+\t// Futex-based impl treats it as uint32 key,
+\t// while sema-based impl as M* waitm.
+\t// Used to be a union, but unions break precise GC.
+\tuintptr	key;
 };
 struct String
 {

コアとなるコードの解説

この変更は、Goランタイムの低レベルな同期プリミティブの内部実装に影響を与えます。Note型は、ゴルーチンが特定のイベントを待機したり、他のゴルーチンに通知したりするためのメカニズムを抽象化しています。

具体的には、lock_futex.cはLinuxのfutexシステムコールを利用した実装であり、lock_sema.cはセマフォ(より抽象的な同期メカニズム)を利用した実装です。どちらの実装も、Note構造体のkeyメンバーを、futexのキーとして(uint32として)、または待機中のM(マシン、つまりOSスレッド)へのポインタとして(M*として)使用していました。

unionからstructへの変更とuintptrの使用は、このkeyメンバーがGCによってポインタとして誤解釈されることを防ぐためのものです。uintptrは、ポインタの値を保持できる整数型であり、GCはこれをポインタとして追跡しません。これにより、GCはNote構造体のメモリを安全にスキャンし、ポインタではないと判断できます。

コード内の((uint32*)&n->key)((void**)&n->key)のようなキャストは、C言語のコンパイラに対して、n->keyが指すメモリ領域を特定の型(uint32またはポインタ)として解釈するように指示しています。これは、GoランタイムのCコードが、uintptr型のkeyを、その使用コンテキストに応じて異なる型として「再解釈」していることを示しています。このアプローチにより、GCの正確性を維持しつつ、低レベルな同期プリミティブの柔軟な実装が可能になります。

関連リンク

参考にした情報源リンク

  • Go issue #5193の議論
  • C言語のunionstructに関する一般的なドキュメント
  • ガベージコレクション(特に正確なGC)に関する一般的な情報
  • Linux futexに関するドキュメント