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

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

このコミットは、GoランタイムにおけるGOTRACEBACK環境変数の設定値の取得を最適化することを目的としています。特に、gotraceback関数の呼び出しが頻繁に行われるガベージコレクション中に、環境変数の読み込みと解析(getenvatoiの呼び出し)によるオーバーヘッドを削減するため、設定値をキャッシュするメカニズムを導入しています。これにより、特にPlan 9のような特定の環境で発生していた潜在的な問題(getenvmallocを呼び出すことによるGC中のメモリ割り当て)を緩和し、他のシステムでもわずかながらパフォーマンスを向上させます。

コミット

runtime: cache gotraceback setting

On Plan 9 gotraceback calls getenv calls malloc, and we gotraceback
on every call to gentraceback, which happens during garbage collection.
Honestly I don't even know how this works on Plan 9.
I suspect it does not, and that we are getting by because
no one has tried to run with $GOTRACEBACK set at all.

This will speed up all the other systems by epsilon, since they
won't call getenv and atoi repeatedly.

LGTM=bradfitz
R=golang-codereviews, bradfitz, 0intro
CC=golang-codereviews
https://golang.org/cl/85430046

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

https://github.com/golang/go/commit/5556bfa9c736f63ae18ec0ab8ef9b6a986e32ef3

元コミット内容

commit 5556bfa9c736f63ae18ec0ab8ef9b6a986e32ef3
Author: Russ Cox <rsc@golang.org>
Date:   Tue Apr 8 22:35:41 2014 -0400

    runtime: cache gotraceback setting

    On Plan 9 gotraceback calls getenv calls malloc, and we gotraceback
    on every call to gentraceback, which happens during garbage collection.
    Honestly I don't even know how this works on Plan 9.
    I suspect it does not, and that we are getting by because
    no one has tried to run with $GOTRACEBACK set at all.

    This will speed up all the other systems by epsilon, since they
    won't call getenv and atoi repeatedly.

    LGTM=bradfitz
    R=golang-codereviews, bradfitz, 0intro
    CC=golang-codereviews
    https://golang.org/cl/85430046
---
 src/pkg/runtime/proc.c    |  4 ++++\n src/pkg/runtime/runtime.c | 35 ++++++++++++++++++++++++-----------\n 2 files changed, 28 insertions(+), 11 deletions(-)\n
diff --git a/src/pkg/runtime/proc.c b/src/pkg/runtime/proc.c
index 2ab54be70c..6b5c031c87 100644
--- a/src/pkg/runtime/proc.c
+++ b/src/pkg/runtime/proc.c
@@ -155,6 +155,10 @@ runtime·schedinit(void)
  	// in a fault during a garbage collection, it will not
  	// need to allocated memory.
  	runtime·newErrorCString(0, &i);\n+\t\n+\t// Initialize the cached gotraceback value, since\n+\t// gotraceback calls getenv, which mallocs on Plan 9.\n+\truntime·gotraceback(nil);\n  \n  	runtime·goargs();
  	runtime·goenvs();
 diff --git a/src/pkg/runtime/runtime.c b/src/pkg/runtime/runtime.c
 index d995bf97ae..725c6d838e 100644
 --- a/src/pkg/runtime/runtime.c
 +++ b/src/pkg/runtime/runtime.c
 @@ -20,22 +20,35 @@ enum {\n int32
 runtime·gotraceback(bool *crash)\n {\n+\t// Keep a cached value to make gotraceback fast,\n+\t// since we call it on every call to gentraceback.\n+\t// The cached value is a uint32 in which the low bit\n+\t// is the \"crash\" setting and the top 31 bits are the\n+\t// gotraceback value.\n+\tstatic uint32 cache = ~(uint32)0;\n \tbyte *p;\n+\tuint32 x;\n \n  \tif(crash != nil)\n  \t\t*crash = false;\n-\tp = runtime·getenv(\"GOTRACEBACK\");\n-\tif(p == nil || p[0] == \'\\0\') {\n-\t\tif(m->traceback != 0)\n-\t\t\treturn m->traceback;\n-\t\treturn 1;\t// default is on\n-\t}\n-\tif(runtime·strcmp(p, (byte*)\"crash\") == 0) {\n-\t\tif(crash != nil)\n-\t\t\t*crash = true;\n-\t\treturn 2;\t// extra information\n+\tif(m->traceback != 0)\n+\t\treturn m->traceback;\n+\tx = runtime·atomicload(&cache);\n+\tif(x == ~(uint32)0) {\n+\t\tp = runtime·getenv(\"GOTRACEBACK\");\n+\t\tif(p == nil)\n+\t\t\tp = (byte*)\"\";\n+\t\tif(p[0] == \'\\0\')\n+\t\t\tx = 1<<1;\n+\t\telse if(runtime·strcmp(p, (byte*)\"crash\") == 0)\n+\t\t\tx = (2<<1) | 1;\n+\t\telse\n+\t\t\tx = runtime·atoi(p)<<1;\t\n+\t\truntime·atomicstore(&cache, x);\n  \t}\n-\treturn runtime·atoi(p);\n+\tif(crash != nil)\n+\t\t*crash = x&1;\n+\treturn x>>1;\n }\n \n int32

変更の背景

このコミットの主な背景には、Goランタイムがスタックトレースを生成する際に使用するGOTRACEBACK環境変数の設定値の取得方法に起因するパフォーマンスと安定性の問題がありました。

特に、Plan 9オペレーティングシステム上では、getenv(環境変数を取得する関数)が内部でメモリ割り当て(malloc)を呼び出すという特性がありました。Goランタイムは、ガベージコレクション(GC)の実行中にgentraceback(トレースバックを生成する関数)を頻繁に呼び出し、その過程でgotraceback関数が呼ばれます。もしgotracebackが呼ばれるたびにgetenvmallocを呼び出すと、GC中にメモリ割り当てが発生するという、非常に危険でデッドロックを引き起こしやすい状況が生じます。コミットメッセージの著者であるRuss Cox氏も、「正直なところ、Plan 9でこれがどのように機能しているのかさえわからない」と述べており、$GOTRACEBACKが設定されていないために問題が顕在化していなかった可能性を指摘しています。

他のシステム(Linux, macOSなど)では、getenvmallocを呼び出すことは一般的ではありませんが、それでもgotracebackが呼ばれるたびに環境変数を読み込み、文字列を整数に変換するatoiを呼び出すことは、わずかながらオーバーヘッドとなります。このコミットは、これらの繰り返し発生する呼び出しを避けることで、すべてのシステムでパフォーマンスを向上させることを目指しました。

しかし、このコミットのレビュー過程では、0intro氏が「GOTRACEBACKgetenvmallocを呼び出すという根本的な問題のために、Plan 9ではこれまで正しく機能したことがなかった」と指摘しています。このコミットによるキャッシュ導入は、この根本問題を解決するものではなく、むしろその存在を浮き彫りにしました。Russ Cox氏も、getenv自体がmallocを避けるように変更される必要がある(例:sbrkを使用するか、静的バッファを使用する)と認めています。

また、このコミットが提出された直後、linux-386ビルダで問題を引き起こしたという報告もあり、変更が意図しない副作用をもたらす可能性も示唆されました。

前提知識の解説

Goランタイム (Go Runtime)

Goランタイムは、Goプログラムの実行を管理するシステムです。これには、ガベージコレクション(GC)、ゴルーチン(軽量スレッド)のスケジューリング、メモリ管理、システムコールとの連携などが含まれます。Goプログラムは、OS上で直接実行されるのではなく、このランタイム上で動作します。

GOTRACEBACK環境変数

GOTRACEBACKは、Goプログラムがパニック(実行時エラー)を起こした際に生成されるスタックトレースの詳しさを制御する環境変数です。

  • GOTRACEBACK=0 または none: スタックトレースを抑制し、パニックメッセージのみを表示します。
  • GOTRACEBACK=1 または single (デフォルト): パニックを引き起こしたゴルーチンのスタックトレースを表示します。
  • GOTRACEBACK=2 または system: ランタイム関数を含む、より詳細なスタックトレースを表示します。
  • GOTRACEBACK=all: すべてのユーザー作成ゴルーチンのスタックトレースを表示します。
  • GOTRACEBACK=crash: オペレーティングシステム固有のクラッシュ(Unix系ではSIGABRTなど)を引き起こし、コアダンプを生成しようとします。

スタックトレース (Stack Trace)

スタックトレースは、プログラムがエラーやパニックを起こした時点での関数呼び出しの履歴です。どの関数がどの関数を呼び出し、最終的にエラーが発生したのかを示すことで、問題の原因を特定するのに役立ちます。

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

ガベージコレクションは、プログラムが動的に確保したメモリのうち、もはや使用されていない(参照されていない)領域を自動的に解放するプロセスです。GoのGCは、プログラムの実行中にバックグラウンドで動作し、メモリリークを防ぎ、開発者が手動でメモリを管理する手間を省きます。GCの実行中は、プログラムの実行が一時停止したり、パフォーマンスに影響を与えたりすることがあります。

Plan 9

Plan 9 from Bell Labsは、Unixの設計思想をさらに推し進めた分散オペレーティングシステムです。その特徴の一つに、すべてのリソース(ファイル、デバイス、ネットワーク接続、さらには環境変数)がファイルシステムとして表現されるという思想があります。例えば、環境変数は/envディレクトリ内のファイルとして扱われます。この特性が、getenvの動作に影響を与えます。

getenvmalloc

  • getenv: C言語の標準ライブラリ関数で、指定された環境変数の値を取得します。通常、この関数は環境変数の値を指すポインタを返しますが、そのメモリはシステムによって管理されており、ユーザーが解放すべきではありません。
  • malloc: C言語の標準ライブラリ関数で、指定されたサイズのメモリブロックをヒープから動的に割り当てます。

一般的なシステムでは、getenvが直接mallocを呼び出すことは稀ですが、Plan 9のように環境変数がファイルとして扱われるシステムでは、環境変数の値を読み込むためにファイルI/Oやバッファリングが必要となり、その過程でメモリ割り当て(malloc)が発生する可能性があります。ガベージコレクション中にmallocが呼ばれることは、GCの内部状態と競合し、デッドロックやクラッシュの原因となる非常に危険な状況です。

atoi

atoiは "ASCII to Integer" の略で、C言語の標準ライブラリ関数の一つです。文字列を整数値に変換するために使用されます。GOTRACEBACK環境変数の値は文字列として設定されるため、Goランタイムがその値を数値として解釈する際にatoiのような変換処理が必要になります。

技術的詳細

このコミットは、GOTRACEBACK環境変数の値をキャッシュすることで、runtime·gotraceback関数の呼び出しコストを削減します。

  1. キャッシュの導入: runtime·gotraceback関数内にstatic uint32 cacheという静的変数が導入されました。この変数は、GOTRACEBACKの設定値を一度読み込んだら、それを記憶しておくためのキャッシュとして機能します。初期値は~(uint32)0(すべてのビットが1)に設定されており、これはキャッシュがまだ初期化されていないことを示します。

  2. キャッシュ値のエンコーディング: uint32型のcache変数には、GOTRACEBACKの数値設定(例: 1, 2, allなど)と、crashフラグ(GOTRACEBACK=crashが設定されているかどうか)の両方がエンコードされます。

    • 最下位ビット(x&1)がcrashフラグを表します。1であればcrashモード、0であればそれ以外です。
    • 残りの31ビット(x>>1)がGOTRACEBACKの数値設定を表します。 例えば、GOTRACEBACK=1の場合は1<<1GOTRACEBACK=crashの場合は(2<<1) | 1(値は2でcrashフラグが立つ)のようにエンコードされます。
  3. キャッシュの利用ロジック: runtime·gotracebackが呼び出されると、まずruntime·atomicload(&cache)を使ってキャッシュの値をアトミックに読み込みます。

    • もし読み込んだ値が初期値~(uint32)0であれば、キャッシュが未初期化であると判断し、実際にruntime·getenv("GOTRACEBACK")を呼び出して環境変数の値を読み込みます。
    • 読み込んだ環境変数の値に基づいて、適切な数値とcrashフラグを計算し、runtime·atomicstore(&cache, x)を使ってアトミックにキャッシュに書き込みます。アトミック操作を使用することで、複数のゴルーチンが同時にgotracebackを呼び出しても、キャッシュの整合性が保たれます。
    • キャッシュが初期化済みであれば、環境変数を再度読み込むことなく、キャッシュされた値からcrashフラグとGOTRACEBACKの数値を抽出し、それを返します。
  4. 初期化時のキャッシュ事前設定: src/pkg/runtime/proc.cruntime·schedinit関数(ランタイムの初期化処理)内で、runtime·gotraceback(nil)が呼び出されるようになりました。これは、ランタイム起動時に一度gotraceback関数を呼び出すことで、Plan 9におけるgetenvmalloc呼び出し問題を回避しつつ、キャッシュを事前に設定しておくためのものです。これにより、GC中に初めてgotracebackが呼ばれる際にgetenvが呼び出されることを防ぎます。

  5. パフォーマンスへの影響: この変更により、gotracebackが頻繁に呼び出される場合(特にGC中)、環境変数の読み込みと解析のオーバーヘッドが初回のみに限定され、以降は高速なキャッシュアクセスに置き換わります。これにより、全体的なパフォーマンスがわずかに向上します。

  6. Plan 9の問題の再確認: このコミットは、Plan 9におけるgetenvmallocを呼び出すという根本的な問題を解決するものではありませんでした。むしろ、キャッシュを導入してもなお、getenvmallocを呼び出すこと自体が問題であり、getenvの実装自体を変更する必要があることが議論で明らかになりました。

  7. ビルドの不具合: このコミットが提出された後、linux-386ビルダで問題が発生したと報告されました。これは、変更が特定のアーキテクチャや環境で予期せぬ副作用を引き起こす可能性を示しています。

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

このコミットによる主要なコード変更は、以下の2つのファイルに集中しています。

  1. src/pkg/runtime/proc.c:

    • runtime·schedinit関数内に、runtime·gotraceback(nil);という行が追加されました。これは、ランタイムの初期化時にgotraceback関数を一度呼び出し、GOTRACEBACK環境変数の値をキャッシュに事前設定するためのものです。
  2. src/pkg/runtime/runtime.c:

    • runtime·gotraceback関数の実装が大幅に変更されました。
    • static uint32 cache = ~(uint32)0;という静的変数が追加され、GOTRACEBACK設定値のキャッシュとして機能します。
    • 環境変数を読み込む前にキャッシュをチェックし、未初期化の場合のみruntime·getenvruntime·atoiを呼び出すロジックが追加されました。
    • キャッシュへの読み書きにはruntime·atomicloadruntime·atomicstoreが使用され、アトミック性が保証されています。
    • GOTRACEBACKの数値とcrashフラグを単一のuint32値にエンコード・デコードするロジックが実装されました。

コアとなるコードの解説

src/pkg/runtime/runtime.cruntime·gotraceback 関数

int32
runtime·gotraceback(bool *crash)
{
	// Keep a cached value to make gotraceback fast,
	// since we call it on every call to gentraceback.
	// The cached value is a uint32 in which the low bit
	// is the "crash" setting and the top 31 bits are the
	// gotraceback value.
	static uint32 cache = ~(uint32)0; // キャッシュ変数、初期値は未初期化を示す
	byte *p;
	uint32 x;

	if(crash != nil)
		*crash = false; // crashフラグの初期化

	if(m->traceback != 0) // m->tracebackが設定されている場合はそれを返す(デバッグ用など)
		return m->traceback;

	x = runtime·atomicload(&cache); // キャッシュの値をアトミックに読み込む
	if(x == ~(uint32)0) { // キャッシュが未初期化の場合
		p = runtime·getenv("GOTRACEBACK"); // 環境変数を読み込む
		if(p == nil)
			p = (byte*)""; // 環境変数が設定されていない場合は空文字列として扱う
		if(p[0] == '\0') // 空文字列の場合(デフォルト値)
			x = 1<<1; // デフォルト値1をエンコード (1 << 1 = 2)
		else if(runtime·strcmp(p, (byte*)"crash") == 0) // "crash"の場合
			x = (2<<1) | 1; // 値2をエンコードし、crashフラグを立てる (2 << 1 | 1 = 5)
		else
			x = runtime·atoi(p)<<1; // 数値の場合、それをエンコード
		runtime·atomicstore(&cache, x); // 計算した値をアトミックにキャッシュに保存
	}
	if(crash != nil)
		*crash = x&1; // キャッシュされた値からcrashフラグをデコード
	return x>>1; // キャッシュされた値からGOTRACEBACKの数値をデコードして返す
}

このコードは、GOTRACEBACK環境変数の値を効率的に取得するためのキャッシュメカニズムを実装しています。

  • static uint32 cache = ~(uint32)0;: cache変数は、runtime·gotraceback関数が呼び出されるたびに再初期化されることなく、その状態を保持します。~(uint32)0という初期値は、すべてのビットが1であるuint32の最大値であり、これは「キャッシュがまだ設定されていない」という特別な意味を持ちます。
  • x = runtime·atomicload(&cache);: 複数のゴルーチンが同時にruntime·gotracebackを呼び出す可能性があるため、キャッシュの読み込みはアトミックに行われます。これにより、データ競合を防ぎます。
  • if(x == ~(uint32)0): キャッシュが未初期化の場合、実際の環境変数読み込みと解析が行われます。
    • p = runtime·getenv("GOTRACEBACK");: 環境変数の値を取得します。
    • if(p == nil) p = (byte*)"";: 環境変数が設定されていない場合は、空文字列として扱います。
    • if(p[0] == '\0') x = 1<<1;: 環境変数が空の場合(デフォルトの動作)、GOTRACEBACK=1を意味する値をxに設定します。1を左に1ビットシフトすることで、最下位ビットをcrashフラグ用に空けています。
    • else if(runtime·strcmp(p, (byte*)"crash") == 0) x = (2<<1) | 1;: 環境変数が"crash"の場合、GOTRACEBACK=2を意味する値を設定し、最下位ビットを1にすることでcrashフラグを立てます。
    • else x = runtime·atoi(p)<<1;: それ以外の場合(数値が設定されている場合)、atoiで文字列を整数に変換し、同様に左に1ビットシフトしてxに設定します。
  • runtime·atomicstore(&cache, x);: 計算されたGOTRACEBACKの値をアトミックにキャッシュに保存します。
  • if(crash != nil) *crash = x&1;: キャッシュされた値xの最下位ビットをマスクすることで、crashフラグを抽出します。
  • return x>>1;: キャッシュされた値xを右に1ビットシフトすることで、crashフラグを除いたGOTRACEBACKの数値設定を返します。

src/pkg/runtime/proc.cruntime·schedinit 関数

// ...
	// Initialize the cached gotraceback value, since
	// gotraceback calls getenv, which mallocs on Plan 9.
	runtime·gotraceback(nil);
// ...

この変更は、Goランタイムの初期化フェーズであるruntime·schedinitにおいて、runtime·gotraceback(nil)を明示的に呼び出すものです。これにより、プログラムの実行が本格的に始まる前にGOTRACEBACKのキャッシュが一度初期化されます。特にPlan 9のようにgetenvmallocを呼び出す環境では、この事前初期化によって、ガベージコレクション中にgotracebackが初めて呼び出される際にmallocが発生するのを防ぎ、潜在的なデッドロックやクラッシュのリスクを軽減します。

関連リンク

参考にした情報源リンク