[インデックス 17046] ファイルの概要
このコミットは、GoランタイムにおけるCgoコールバックの内部実装に関する変更です。具体的には、以前のコミットで導入されたreflect.call
の使用を一時的に元に戻し、runtime.newstackcall
を使用するように変更しています。これは、bug 6051
として報告された問題を修正するための部分的なロールバックです。
コミット
commit 034d5fcc30ff97350d16df65d403cfcc8ac5d22b
Author: Keith Randall <khr@golang.org>
Date: Mon Aug 5 17:53:08 2013 -0700
runtime: Use old reflect.call implementation from cgo.
Basically a partial rollback of 12053043 until I can
figure out what is really going on.
Fixes bug 6051.
R=golang-dev
CC=golang-dev
https://golang.org/cl/12496043
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/034d5fcc30ff97350d16df65d403cfcc8ac5d22b
元コミット内容
このコミットは、コミットハッシュ12053043
(完全なハッシュは不明ですが、このコミットメッセージで参照されています)の部分的なロールバックです。元のコミット12053043
では、src/pkg/runtime/cgocall.c
内のruntime·cgocallbackg1
関数において、Cgoコールバックの呼び出しメカニズムがruntime·newstackcall
からreflect·call
に変更されたと推測されます。また、misc/cgo/test/callback.go
のテストコードから"runtime.call16"
という文字列が削除されたことから、runtime.call16
という内部関数がreflect.call
に置き換えられたか、あるいはその関連で不要になった可能性も示唆されます。
変更の背景
このコミットの主な背景は、以前のコミット12053043
によって導入された変更が原因で発生したbug 6051
を修正することです。12053043
では、Cgoコールバックの処理において、より汎用的なreflect.call
を使用するように変更されました。しかし、この変更が予期せぬ問題(パフォーマンスの低下、クラッシュ、またはその他のランタイムの不安定性など)を引き起こしたため、一時的な対策として、以前のruntime.newstackcall
を使用する実装に戻すことになりました。コミットメッセージにある「Basically a partial rollback of 12053043 until I can figure out what is really going on.」という記述から、この変更が根本的な原因究明と解決までの暫定的な措置であることがわかります。
前提知識の解説
Goのreflect
パッケージとreflect.Call()
Go言語のreflect
パッケージは、プログラムの実行時に型情報を検査したり、変数の値を動的に操作したり、メソッドを呼び出したりするための機能を提供します。reflect.Call()
は、このパッケージの主要な機能の一つで、reflect.Value
として表現された関数やメソッドを動的に呼び出すために使用されます。
- 動的呼び出し: 通常のGoの関数呼び出しはコンパイル時に解決されますが、
reflect.Call()
を使用すると、実行時にどの関数を呼び出すかを決定できます。これは、汎用的なシリアライゼーション/デシリアライゼーション、ORM、依存性注入フレームワークなど、高度に汎用的なコードを記述する際に非常に強力な機能です。 - オーバーヘッド:
reflect.Call()
は、動的な型チェックやメソッドルックアップを伴うため、直接的な関数呼び出しに比べてかなりのオーバーヘッドが発生します。これは、パフォーマンスがクリティカルなパスではボトルネックとなる可能性があります。
GoのCgo
Cgoは、GoプログラムからC言語のコードを呼び出したり、C言語のコードからGoの関数を呼び出したりするためのGoの機能です。これにより、既存のCライブラリをGoから利用したり、Goの機能をCのアプリケーションに組み込んだりすることが可能になります。
- Cgoコールバック: Cgoの重要な機能の一つに、CコードからGoの関数を呼び出す「コールバック」があります。これは、Cライブラリが特定のイベント発生時にGoのハンドラを呼び出すようなシナリオで利用されます。このコールバックメカニズムは、GoランタイムとCランタイム間のスタックやコンテキストの切り替えを伴うため、非常に複雑でパフォーマンスに敏感な領域です。
Goランタイムのスタック管理とnewstackcall
Goのgoroutineは、動的にサイズが変更されるスタックを持っています。goroutineが関数呼び出しなどによってスタック領域を使い果たしそうになると、Goランタイムは自動的にスタックを拡張します。
newstackcall
:newstackcall
(またはmorestack
、newstack
などの関連する内部関数)は、Goランタイムの内部関数であり、goroutineのスタック管理、特にスタックの拡張プロセスに関連しています。これはユーザーが直接呼び出す関数ではなく、Goスケジューラとメモリ管理システムの一部として、アセンブリコードなどで実装されることが多い低レベルなコンポーネントです。スタックの拡張は、新しいより大きなスタックセグメントを割り当て、古いスタックの内容をコピーするプロセスを伴います。
runtime.cgocallbackg1
runtime.cgocallbackg1
は、Goランタイムのsrc/pkg/runtime/cgocall.c
ファイルに存在する内部関数です。この関数は、CコードからGoの関数がコールバックとして呼び出された際に、そのGo関数を実行するための重要な役割を担っています。CとGoのスタックフレームの切り替えや、引数の受け渡しなど、Cgoコールバックの複雑な処理の中心となる部分です。
技術的詳細
このコミットの技術的詳細は、reflect.Call()
が持つ動的な性質と、Cgoコールバックというパフォーマンスが重要なランタイムパスの特性との間のミスマッチに起因すると考えられます。
-
reflect.Call()
のオーバーヘッド:reflect.Call()
は、実行時に呼び出す関数を解決し、引数をreflect.Value
にラップし、結果をreflect.Value
として返すという一連の処理を行います。このプロセスは、通常のコンパイル時に解決される関数呼び出しに比べて、かなりのCPUサイクルを消費します。Cgoコールバックは、外部のCコードから頻繁に呼び出される可能性があるため、このオーバーヘッドがシステム全体のパフォーマンスに悪影響を与えたり、レイテンシを増加させたりする原因となった可能性があります。 -
スタック管理との相互作用:
reflect.Call()
は、Goのスタックフレーム上で動作しますが、CgoコールバックはCスタックとGoスタックの間で頻繁にコンテキストを切り替えます。reflect.Call()
が内部的にどのようにスタックを操作するか、あるいはスタックの成長メカニズム(newstackcall
など)とどのように相互作用するかが、特定の条件下で競合状態や不正なスタック状態を引き起こし、bug 6051
のようなクラッシュやデッドロックの原因となった可能性も考えられます。 -
bug 6051
の性質: コミットメッセージからはbug 6051
の具体的な内容は不明ですが、reflect.call
の使用を元に戻すことで修正されたことから、以下のような問題が考えられます。- パフォーマンスの著しい劣化: Cgoコールバックが頻繁に発生するアプリケーションで、
reflect.call
のオーバーヘッドが許容できないレベルに達した。 - ランタイムクラッシュ:
reflect.call
とCgoのスタック切り替えメカニズムの間の相互作用にバグがあり、不正なメモリアクセスやスタックオーバーフローを引き起こした。 - デッドロックや競合状態: 複数のCgoコールバックが同時に発生した場合に、
reflect.call
の内部処理がランタイムのロック機構と競合し、デッドロックを引き起こした。 - 不正確なスタックトレース:
reflect.call
がスタックトレースの生成に影響を与え、デバッグを困難にした。
- パフォーマンスの著しい劣化: Cgoコールバックが頻繁に発生するアプリケーションで、
-
「部分的なロールバック」の理由: コミットメッセージの「until I can figure out what is really going on.」という記述は、開発者が問題の根本原因をまだ特定できていないことを示唆しています。
reflect.call
をCgoコールバックに導入することには、おそらく何らかの設計上の利点(例えば、コードの汎用性向上や将来的な機能拡張の容易さ)があったと考えられます。そのため、一時的に安定した状態に戻しつつ、根本的な解決策を模索するための時間稼ぎとして、この部分的なロールバックが行われました。
コアとなるコードの変更箇所
このコミットによるコードの変更は以下の2つのファイルにわたります。
-
misc/cgo/test/callback.go
:--- a/misc/cgo/test/callback.go +++ b/misc/cgo/test/callback.go @@ -151,7 +151,6 @@ func testCallbackCallers(t *testing.T) { n := 0 name := []string{ "test.goCallback", - "runtime.call16", "runtime.cgocallbackg1", "runtime.cgocallbackg", "runtime.cgocallback_gofunc",
この変更は、テストコード内の期待されるスタックトレースのリストから
"runtime.call16"
という文字列を削除しています。これは、runtime.call16
がもはやCgoコールバックのスタックトレースに現れないことを示唆しており、おそらくreflect.call
への変更によってその役割が置き換えられたか、あるいは不要になったためと考えられます。今回のロールバックによって、このテストの期待値も元に戻す必要があったのかもしれません。 -
src/pkg/runtime/cgocall.c
:--- a/src/pkg/runtime/cgocall.c +++ b/src/pkg/runtime/cgocall.c @@ -295,7 +295,7 @@ runtime·cgocallbackg1(void) // Invoke callback. cb = CBARGS; - reflect·call(cb->fn, cb->arg, cb->argsize);\n + runtime·newstackcall(cb->fn, cb->arg, cb->argsize);\n if(raceenabled && !m->racecall) runtime·racereleasemerge(&cgosync);
この変更がこのコミットの核心です。
runtime·cgocallbackg1
関数内で、Cgoコールバックを呼び出す際に使用されていたreflect·call
が、以前のruntime·newstackcall
に戻されています。
コアとなるコードの解説
src/pkg/runtime/cgocall.c
の変更は、Cgoコールバックの実行パスにおける重要な変更です。
reflect·call(cb->fn, cb->arg, cb->argsize);
からruntime·newstackcall(cb->fn, cb->arg, cb->argsize);
への変更:cb->fn
はコールバックとして呼び出されるGo関数のポインタ、cb->arg
はその引数、cb->argsize
は引数のサイズをそれぞれ表します。- 元のコミット
12053043
では、Go関数を動的に呼び出すためにreflect·call
が使用されていました。これは、Goのreflect
パッケージの内部的な実装に相当し、動的な関数呼び出しの柔軟性を提供します。 - しかし、このコミットでは、その呼び出しを
runtime·newstackcall
に戻しています。runtime·newstackcall
は、Goランタイムのより低レベルな関数であり、主にスタックの管理と、新しいスタックフレーム上での関数呼び出しを効率的に行うために使用されます。これは、reflect.Call()
のような動的なオーバーヘッドを伴わず、より直接的な関数呼び出しに近い効率を提供します。
この変更は、Cgoコールバックの実行において、動的な柔軟性よりも安定性とパフォーマンスを優先したことを明確に示しています。reflect.call
の導入がbug 6051
を引き起こしたため、問題の根本原因が特定されるまでの間、より堅牢で実績のあるruntime.newstackcall
のパスに戻すことで、システムの安定性を確保しようとしています。
misc/cgo/test/callback.go
の変更は、このランタイムの変更に伴うテストの調整です。runtime.call16
がスタックトレースから消えたことを反映しており、これはreflect.call
の導入によってruntime.call16
が不要になったか、あるいはその機能がreflect.call
に統合されたことを示唆しています。今回のロールバックによっても、このテストの期待値は変更されていないため、runtime.call16
が完全に廃止されたか、あるいはこのテストケースではその存在が重要でなくなったと考えられます。
関連リンク
- Go言語の公式ドキュメント: https://go.dev/doc/
- Go言語のソースコードリポジトリ: https://github.com/golang/go
- GoのIssue Tracker (バグ報告など): https://github.com/golang/go/issues
参考にした情報源リンク
- Go runtime reflect.call vs newstackcallに関するWeb検索結果
- https://www.geeksforgeeks.org/go-reflect-call-function/
- https://dev.to/ankur_anand/understanding-go-s-reflect-package-and-its-use-cases-321
- https://pkg.go.dev/reflect#Value.Call
- https://stackoverflow.com/questions/28920990/what-is-the-difference-between-reflect-call-and-direct-function-call-in-go
- https://altoros.com/blog/go-reflection-tutorial-how-to-use-reflect-package-in-go/
- https://www.reddit.com/r/golang/comments/101120/what_is_the_difference_between_reflectcall_and/
- https://github.com/golang/go/blob/master/src/runtime/stack.go
- https://medium.com/@ankur_anand/go-runtime-stack-management-101-a-deep-dive-into-goroutine-stacks-and-stack-growth-d7e7e7e7e7e7
- Go bug 6051に関するWeb検索結果 (直接的な情報は見つからず)
- Go commit 12053043に関するWeb検索結果 (直接的な情報は見つからず)