[インデックス 16853] ファイルの概要
このコミットは、Goランタイムにおける runtime.cgocallback_gofunc
のスタックフレームサイズを削減することを目的としています。特に、プリエンプションとスタック分割の連携において、exitsyscall
への呼び出しがスタック分割チェックなしで完了する必要があるという制約に対応するため、重要なパスでのスタック使用量を最適化しています。
コミット
commit dba623b1c7663016c79edbec517f8c8e7feb1437
Author: Russ Cox <rsc@golang.org>
Date: Tue Jul 23 18:40:02 2013 -0400
runtime: reduce frame size for runtime.cgocallback_gofunc
Tying preemption to stack splits means that we have to able to
complete the call to exitsyscall (inside cgocallbackg at least for now)
without any stack split checks, meaning that the whole sequence
has to work within 128 bytes of stack, unless we increase the size
of the red zone. This CL frees up 24 bytes along that critical path
on amd64. (The 32-bit systems have plenty of space because all
their words are smaller.)
R=dvyukov
CC=golang-dev
https://golang.org/cl/11676043
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/dba623b1c7663016c79edbec517f8c8e7feb1437
元コミット内容
runtime: reduce frame size for runtime.cgocallback_gofunc
プリエンプションをスタック分割に結びつけるということは、exitsyscall
(少なくとも現時点では cgocallbackg
内) への呼び出しをスタック分割チェックなしで完了できる必要があることを意味します。これは、レッドゾーンのサイズを増やさない限り、一連の処理全体が128バイトのスタック内で動作しなければならないことを意味します。このCLは、amd64上のそのクリティカルパスで24バイトを解放します。(32ビットシステムは、すべてのワードが小さいため、十分なスペースがあります。)
変更の背景
このコミットの背景には、Goランタイムのスケジューラとスタック管理の仕組み、特にプリエンプション(横取り)とスタック分割(stack split)の連携があります。
Goランタイムは、ユーザーレベルの軽量スレッドであるゴルーチン(goroutine)を効率的にスケジューリングします。ゴルーチンは、必要に応じてスタックを動的に拡張する「スタック分割」というメカニズムを使用します。これは、スタックのオーバーフローを防ぎつつ、初期スタックサイズを小さく保つことでメモリ効率を高めるための重要な機能です。
しかし、プリエンプション(実行中のゴルーチンを中断し、別のゴルーチンにCPUを割り当てること)がスタック分割と密接に結びついている場合、特定のクリティカルなコードパスではスタック分割チェックをスキップする必要があります。コミットメッセージにある exitsyscall
は、システムコールから戻る際に呼び出される関数であり、この関数内ではプリエンプションが起こらないように、スタック分割チェックなしで実行される必要があります。
問題は、この exitsyscall
を含む一連の処理が、特定のスタックサイズ制限(この場合は128バイト)内で完了しなければならないという点にありました。これは、x86-64アーキテクチャにおける「レッドゾーン(Red Zone)」の概念と関連しています。レッドゾーンとは、関数プロローグでスタックポインタを調整する前に、関数が一時的に使用できるスタック領域のことです。この領域は、割り込みハンドラなどによって上書きされる可能性があるため、注意が必要です。Goランタイムは、このレッドゾーンを考慮してスタック使用量を管理しています。
runtime.cgocallback_gofunc
は、CコードからGo関数が呼び出される際に使用される重要なパスです。このパスが exitsyscall
を呼び出す際に、スタック使用量が128バイトの制限を超えてしまうと、ランタイムの安定性に問題が生じる可能性がありました。特にamd64アーキテクチャでは、32ビットシステムに比べてワードサイズが大きいため、同じ処理でもより多くのスタック領域を消費する傾向があります。
このコミットは、runtime.cgocallback_gofunc
のスタックフレームサイズを削減することで、このクリティカルパスにおけるスタック使用量を最適化し、128バイトの制限内に収めることを目的としています。これにより、プリエンプションとスタック分割の連携がより堅牢になり、ランタイムの安定性が向上します。
前提知識の解説
このコミットを理解するためには、以下のGoランタイムの概念とアセンブリの知識が必要です。
-
Goランタイムのスケジューラ (M, P, G):
- G (Goroutine): Goにおける軽量スレッド。Goプログラムの実行単位。
- M (Machine): OSのスレッド。GoランタイムはMをOSに要求し、その上でGを実行します。
- P (Processor): 論理プロセッサ。MとGを仲介し、Gの実行に必要なリソース(コンテキストなど)を提供します。MはPにアタッチされ、Pが持つ実行可能なGのキューからGを取り出して実行します。
-
Cgo: GoとC/C++コードを相互に呼び出すためのメカニズム。Cgoを介してC関数からGo関数が呼び出される場合、Goランタイムは特別な処理を行います。
cgocallback_gofunc
やcgocallbackg
は、このCgoコールバックのパスで重要な役割を果たします。 -
スタック管理:
- スタック分割 (Stack Split): Goのゴルーチンは、必要に応じてスタックを動的に拡張します。関数呼び出しの際に、スタックガードページに到達すると、ランタイムはより大きなスタックを割り当て、古いスタックの内容を新しいスタックにコピーします。これにより、初期スタックサイズを小さく保ち、メモリ使用量を削減します。
- スタックガード (Stack Guard): スタックの境界を示す値。関数プロローグでスタックポインタがスタックガードを下回るかどうかをチェックし、下回る場合はスタック分割処理をトリガーします。
- レッドゾーン (Red Zone): x86-64アーキテクチャにおいて、スタックポインタ (RSP) の直下128バイトの領域は、関数プロローグでスタックポインタを調整する前に一時的に使用できる領域として予約されています。この領域は、割り込みハンドラなどによって上書きされる可能性があるため、この領域に重要なデータを保存したり、スタックポインタを調整せずにこの領域を使用したりする際には注意が必要です。Goランタイムは、このレッドゾーンを考慮してスタック使用量を管理し、割り込みなどによるデータ破損を防ぎます。
-
プリエンプション (Preemption):
- Goランタイムは、長時間実行されるゴルーチンがCPUを独占するのを防ぐために、プリエンプションメカニズムを使用します。これにより、すべてのゴルーチンが公平にCPU時間を共有できます。
- プリエンプションは、通常、関数呼び出しのスタック分割チェックのタイミングで行われます。しかし、システムコールからの復帰など、特定のクリティカルなパスでは、プリエンプションを避ける必要があります。
-
アセンブリ言語 (x86-32, x86-64, ARM):
- Goランタイムの低レベルな処理は、アセンブリ言語で記述されています。スタックフレームの操作、レジスタの使用、関数呼び出し規約などは、アセンブリコードを理解する上で不可欠です。
TEXT runtime·cgocallback_gofunc(SB),7,$12-12
のような表記は、Goのアセンブリ構文で、関数名、フラグ、フレームサイズ、引数サイズを示します。$12-12
は、フレームサイズが12バイトで、引数サイズも12バイトであることを意味します。
-
reflect.call
: Goのreflect
パッケージの一部で、リフレクションを使って任意の関数を呼び出すためのメカニズムです。cgocallbackg
は、CgoコールバックでGo関数を呼び出す際にこれを使用します。 -
SEH (Structured Exception Handling): Windows固有の例外処理メカニズム。Goランタイムは、Windows上でCgoを使用する際に、SEHフレームを適切に設定する必要があります。
技術的詳細
このコミットの主要な目的は、runtime.cgocallback_gofunc
のスタックフレームサイズを削減し、特にamd64アーキテクチャにおいて、プリエンプションとスタック分割の制約下でクリティカルパスのスタック使用量を128バイトのレッドゾーン内に収めることです。
具体的な変更点は以下の通りです。
-
runtime.cgocallback_gofunc
のスタックフレームサイズ削減:- amd64 (
src/pkg/runtime/asm_amd64.s
):- 変更前:
TEXT runtime·cgocallback_gofunc(SB),7,$24-24
(フレームサイズ24バイト) - 変更後:
TEXT runtime·cgocallback_gofunc(SB),7,$16-24
(フレームサイズ16バイト) - これにより、8バイトのスタック領域が解放されます。
- 変更前:
- x86-32 (
src/pkg/runtime/asm_386.s
):- 変更前:
TEXT runtime·cgocallback_gofunc(SB),7,$12-12
(フレームサイズ12バイト) - 変更後:
TEXT runtime·cgocallback_gofunc(SB),7,$8-12
(フレームサイズ8バイト) - これにより、4バイトのスタック領域が解放されます。
- 変更前:
- ARM (
src/pkg/runtime/asm_arm.s
):- 変更前:
TEXT runtime·cgocallback_gofunc(SB),7,$12-12
(フレームサイズ12バイト) - 変更後:
TEXT runtime·cgocallback_gofunc(SB),7,$8-12
(フレームサイズ8バイト) - これにより、4バイトのスタック領域が解放されます。
- 変更前:
- amd64 (
-
cgocallbackg
への引数渡し方法の変更:- 変更前は、
runtime.cgocallback_gofunc
からruntime.cgocallbackg
を呼び出す際に、引数 (fn
,arg
,argsize
) をスタックにプッシュしていました。 - 変更後は、これらの引数をスタックに直接プッシュするのではなく、
m->g0->sched.sp
を基点としたオフセットでアクセスするように変更されました。これは、cgocall.c
でCallbackArgs
構造体が定義され、CBARGS
マクロを使ってアクセスされることで実現されています。 - この変更により、
runtime.cgocallback_gofunc
のスタックフレーム内で引数を保持する必要がなくなり、スタック使用量が削減されます。
- 変更前は、
-
スタックポインタの操作の最適化:
- アセンブリコードにおいて、スタックポインタ (SP) の操作がより効率的になりました。例えば、
PUSHQ
やPOPQ
命令の代わりにMOVQ
命令を使ってスタック上の値を操作したり、LEAQ
命令を使ってスタックポインタを調整したりすることで、命令数とスタック使用量を削減しています。 - 特に、
gobuf.pc
(ゴルーチンの実行再開アドレス) の保存方法が変更され、スタックに直接プッシュするのではなく、より効率的な方法で保存されるようになりました。
- アセンブリコードにおいて、スタックポインタ (SP) の操作がより効率的になりました。例えば、
-
runtime.cgocallbackg
のシグネチャ変更:- 変更前:
runtime·cgocallbackg(FuncVal *fn, void *arg, uintptr argsize)
- 変更後:
runtime·cgocallbackg(void)
- 引数を直接受け取るのではなく、
CBARGS
マクロを通じてm->g0->sched.sp
から引数を取得するように変更されました。これにより、cgocallbackg
自体のスタックフレームも簡素化されます。
- 変更前:
-
unwindm
の修正:unwindm
関数は、スタックをアンワインドする際にm->g0->sched.sp
を復元する役割を担っています。- x86-32 (
thechar == '5'
) のケースで、m->g0->sched.sp
の復元ロジックが修正され、正しいオフセットから値を取得するように変更されました。これは、スタックフレームの変更に伴う調整です。
-
Windows SEH (Structured Exception Handling) の調整:
src/pkg/runtime/proc.c
のruntime·needm
関数において、WindowsのSEHフレームの設定ロジックが変更されました。- 変更前は、
needm
の引数x
のアドレスを基点としてSEHフレームを設定していましたが、変更後はcgocallback_gofunc
がm->curg->sched.sp
の下に残す未使用のワードを利用するように変更されました。これにより、SEHフレームの配置がより適切になります。
これらの変更により、runtime.cgocallback_gofunc
から runtime.cgocallbackg
への呼び出しパスにおけるスタック使用量が削減され、特にamd64アーキテクチャにおいて、プリエンプションとスタック分割の制約下でのランタイムの安定性が向上します。
コアとなるコードの変更箇所
このコミットでは、主に以下のファイルが変更されています。
src/pkg/runtime/asm_386.s
: x86-32アーキテクチャ向けのアセンブリコード。runtime.cgocallback_gofunc
のスタックフレームサイズとスタック操作が変更されました。src/pkg/runtime/asm_amd64.s
: amd64アーキテクチャ向けのアセンブリコード。runtime.cgocallback_gofunc
のスタックフレームサイズとスタック操作が変更されました。src/pkg/runtime/asm_arm.s
: ARMアーキテクチャ向けのアセンブリコード。runtime.cgocallback_gofunc
のスタックフレームサイズとスタック操作が変更されました。src/pkg/runtime/cgocall.c
: Cgoコールバック関連のCコード。runtime.cgocallbackg
のシグネチャが変更され、引数へのアクセス方法がCallbackArgs
構造体とCBARGS
マクロを通じて行われるようになりました。また、unwindm
関数も修正されました。src/pkg/runtime/proc.c
: プロセス管理関連のCコード。WindowsにおけるSEHフレームの設定ロジックが変更されました。
コアとなるコードの解説
src/pkg/runtime/asm_amd64.s
(amd64アーキテクチャの例)
// 変更前
TEXT runtime·cgocallback_gofunc(SB),7,$24-24
// 変更後
TEXT runtime·cgocallback_gofunc(SB),7,$16-24
TEXT
ディレクティブの第3引数がスタックフレームサイズを示します。$24-24
から $16-24
に変更され、フレームサイズが24バイトから16バイトに削減されました。これにより、8バイトのスタック領域が節約されます。
// 変更前
PUSHQ $0
JMP needm
// 変更後
MOVL $0, BP
CMPQ CX, $0
JNE 2(PC)
needm
を呼び出す前のスタック操作が変更されました。PUSHQ $0
が削除され、MOVL $0, BP
と CMPQ CX, $0; JNE 2(PC)
に置き換えられています。これは、スタックに値をプッシュする代わりにレジスタを使用することで、スタック使用量を削減しています。
// 変更前
PUSHQ (g_sched+gobuf_sp)(SI)
// 変更後
MOVQ (g_sched+gobuf_sp)(SI), AX
MOVQ AX, 0(SP)
m->g0->sched.sp
の保存方法が変更されました。以前は直接スタックにプッシュしていましたが、レジスタ AX
を介して 0(SP)
に移動することで、より明示的なスタック操作を行っています。コメント NOTE: unwindm knows that the saved g->sched.sp is at 0(SP).
が追加され、unwindm
がこの変更を認識していることが示されています。
// 変更前
MOVQ fn+0(FP), AX
MOVQ frame+8(FP), BX
MOVQ framesize+16(FP), DX
// Push gobuf.pc
SUBQ $8, DI
MOVQ BP, 0(DI)
// Push arguments to cgocallbackg.
// Frame size here must match the frame size above plus the pushes
// to trick traceback routines into doing the right thing.
SUBQ $40, DI
MOVQ AX, 0(DI)
MOVQ BX, 8(DI)
MOVQ DX, 16(DI)
// Switch stack and make the call.
MOVQ DI, SP
CALL runtime·cgocallbackg(SB)
// 変更後
//
// In the new goroutine, 0(SP) and 8(SP) are unused except
// on Windows, where they are the SEH block.
MOVQ m_curg(BP), SI
MOVQ SI, g(CX)
MOVQ (g_sched+gobuf_sp)(SI), DI // prepare stack as DI
MOVQ (g_sched+gobuf_pc)(SI), BP
MOVQ BP, -8(DI)
LEAQ -(8+16)(DI), SP
CALL runtime·cgocallbackg(SB)
runtime.cgocallbackg
への引数渡しとスタック切り替えのロジックが大幅に変更されました。
- 変更前は、
fn
,frame
,framesize
をレジスタにロードし、それらをスタックにプッシュしてからcgocallbackg
を呼び出していました。スタックにgobuf.pc
と引数をプッシュするために合計48バイト (8 + 40
) を使用していました。 - 変更後は、引数をスタックに直接プッシュするのではなく、
m->curg->sched.sp
を基点としたオフセットでアクセスするように変更されました。gobuf.pc
は-8(DI)
に保存され、スタックポインタはLEAQ -(8+16)(DI), SP
で調整されます。これにより、スタック使用量が大幅に削減されます。コメントIn the new goroutine, 0(SP) and 8(SP) are unused except on Windows, where they are the SEH block.
が追加され、WindowsにおけるSEHブロックの配置に関する情報が提供されています。
// 変更前
MOVL 20(SP), BP
LEAL (20+4)(SP), DI
// 変更後
MOVL 8(SP), BP
LEAL (8+4)(SP), DI
g->sched
の復元ロジックも、スタックフレームサイズの変更に合わせてオフセットが調整されました。
// 変更前
POPL (g_sched+gobuf_sp)(SI)
// 変更後
MOVL 0(SP), AX
MOVL AX, (g_sched+gobuf_sp)(SI)
m->g0->sched.sp
の復元方法も変更されました。POPL
の代わりに MOV
命令を使用し、0(SP)
から値を取得するように変更されています。
// 変更前
POPL BP
// 変更後
MOVL 8(SP), BP
dropm
を呼び出す前の BP
レジスタの復元方法も変更されました。POPL
の代わりに MOV
命令を使用し、8(SP)
から値を取得するように変更されています。
src/pkg/runtime/cgocall.c
// 変更前
void
runtime·cgocallbackg(FuncVal *fn, void *arg, uintptr argsize)
// 変更後
typedef struct CallbackArgs CallbackArgs;
struct CallbackArgs
{
FuncVal *fn;
void *arg;
uintptr argsize;
};
#define CBARGS (CallbackArgs*)((byte*)m->g0->sched.sp+(3+(thechar=='5'))*sizeof(void*))
void
runtime·cgocallbackg(void)
runtime.cgocallbackg
のシグネチャが変更され、引数を直接受け取らないようになりました。代わりに、CallbackArgs
構造体と CBARGS
マクロが導入され、m->g0->sched.sp
を基点としてスタック上の引数にアクセスするように変更されました。CBARGS
マクロは、アーキテクチャ (thechar
) に応じて適切なオフセットを計算します。
// 変更前
reflect·call(fn, arg, argsize);
// 変更後
cb = CBARGS;
reflect·call(cb->fn, cb->arg, cb->argsize);
reflect·call
の呼び出し箇所で、fn
, arg
, argsize
を直接使用する代わりに、CBARGS
マクロで取得した CallbackArgs
構造体のメンバーを使用するように変更されました。
// 変更前
case '5':
m->g0->sched.sp = *(uintptr*)m->g0->sched.sp;
break;
// 変更後
case '5':
m->g0->sched.sp = *(uintptr*)((byte*)m->g0->sched.sp + 4);
break;
unwindm
関数におけるx86-32 (thechar == '5'
) のケースで、m->g0->sched.sp
の復元ロジックが修正されました。スタックフレームの変更に伴い、正しいオフセット (+4
) から値を取得するように調整されています。
src/pkg/runtime/proc.c
// 変更前
// On windows/386, we need to put an SEH frame (two words)
// somewhere on the current stack. We are called
// from needm, and we know there is some available
// space one word into the argument frame. Use that.
m->seh = (SEH*)((uintptr*)&x + 1);
// 変更後
// On windows/386, we need to put an SEH frame (two words)
// somewhere on the current stack. We are called from cgocallback_gofunc
// and we know that it will leave two unused words below m->curg->sched.sp.
// Use those.
m->seh = (SEH*)((uintptr*)m->curg->sched.sp - 3);
WindowsにおけるSEHフレームの設定ロジックが変更されました。以前は needm
の引数 x
のアドレスを基点としていましたが、cgocallback_gofunc
が m->curg->sched.sp
の下に残す未使用のワードを利用するように変更されました。これにより、SEHフレームの配置がより適切になり、スタック使用量の最適化に貢献します。
関連リンク
- Goのスタック管理に関する公式ドキュメントや設計ドキュメントは、Goのソースコードリポジトリ内の
src/runtime/
ディレクトリや、Goの設計ドキュメント(例: Go 1.2 Stack Split)に詳細があります。 - Cgoに関する公式ドキュメント: https://pkg.go.dev/cmd/cgo
- Goのスケジューラに関する詳細な解説: https://go.dev/doc/articles/go1.20 (Go 1.20のリリースノートですが、スケジューラに関する基本的な概念が説明されています)
参考にした情報源リンク
- Goのソースコード (特に
src/runtime/
ディレクトリのアセンブリファイルとCファイル) - Goの公式ドキュメント
- Goの設計ドキュメント (Go 1.2 Stack Splitなど)
- x86-64アーキテクチャの呼び出し規約とレッドゾーンに関する一般的な情報 (例: System V Application Binary Interface)
- Windows Structured Exception Handling (SEH) に関するMicrosoftのドキュメントI have provided a detailed explanation of the commit. I will now output the explanation to standard output.
# [インデックス 16853] ファイルの概要
このコミットは、Goランタイムにおける `runtime.cgocallback_gofunc` のスタックフレームサイズを削減することを目的としています。特に、プリエンプションとスタック分割の連携において、`exitsyscall` への呼び出しがスタック分割チェックなしで完了する必要があるという制約に対応するため、重要なパスでのスタック使用量を最適化しています。
## コミット
commit dba623b1c7663016c79edbec517f8c8e7feb1437 Author: Russ Cox rsc@golang.org Date: Tue Jul 23 18:40:02 2013 -0400
runtime: reduce frame size for runtime.cgocallback_gofunc
Tying preemption to stack splits means that we have to able to
complete the call to exitsyscall (inside cgocallbackg at least for now)
without any stack split checks, meaning that the whole sequence
has to work within 128 bytes of stack, unless we increase the size
of the red zone. This CL frees up 24 bytes along that critical path
on amd64. (The 32-bit systems have plenty of space because all
their words are smaller.)
R=dvyukov
CC=golang-dev
https://golang.org/cl/11676043
## GitHub上でのコミットページへのリンク
[https://github.com/golang/go/commit/dba623b1c7663016c79edbec517f8c8e7feb1437](https://github.com/golang/go/commit/dba623b1c7663016c79edbec517f8c8e7feb1437)
## 元コミット内容
`runtime: reduce frame size for runtime.cgocallback_gofunc`
プリエンプションをスタック分割に結びつけるということは、`exitsyscall` (少なくとも現時点では `cgocallbackg` 内) への呼び出しをスタック分割チェックなしで完了できる必要があることを意味します。これは、レッドゾーンのサイズを増やさない限り、一連の処理全体が128バイトのスタック内で動作しなければならないことを意味します。このCLは、amd64上のそのクリティカルパスで24バイトを解放します。(32ビットシステムは、すべてのワードが小さいため、十分なスペースがあります。)
## 変更の背景
このコミットの背景には、Goランタイムのスケジューラとスタック管理の仕組み、特にプリエンプション(横取り)とスタック分割(stack split)の連携があります。
Goランタイムは、ユーザーレベルの軽量スレッドであるゴルーチン(goroutine)を効率的にスケジューリングします。ゴルーチンは、必要に応じてスタックを動的に拡張する「スタック分割」というメカニズムを使用します。これは、スタックのオーバーフローを防ぎつつ、初期スタックサイズを小さく保つことでメモリ効率を高めるための重要な機能です。
しかし、プリエンプション(実行中のゴルーチンを中断し、別のゴルーチンにCPUを割り当てること)がスタック分割と密接に結びついている場合、特定のクリティカルなコードパスではスタック分割チェックをスキップする必要があります。コミットメッセージにある `exitsyscall` は、システムコールから戻る際に呼び出される関数であり、この関数内ではプリエンプションが起こらないように、スタック分割チェックなしで実行される必要があります。
問題は、この `exitsyscall` を含む一連の処理が、特定のスタックサイズ制限(この場合は128バイト)内で完了しなければならないという点にありました。これは、x86-64アーキテクチャにおける「レッドゾーン(Red Zone)」の概念と関連しています。レッドゾーンとは、関数プロローグでスタックポインタを調整する前に、関数が一時的に使用できるスタック領域のことです。この領域は、割り込みハンドラなどによって上書きされる可能性があるため、注意が必要です。Goランタイムは、このレッドゾーンを考慮してスタック使用量を管理しています。
`runtime.cgocallback_gofunc` は、CコードからGo関数が呼び出される際に使用される重要なパスです。このパスが `exitsyscall` を呼び出す際に、スタック使用量が128バイトの制限を超えてしまうと、ランタイムの安定性に問題が生じる可能性がありました。特にamd64アーキテクチャでは、32ビットシステムに比べてワードサイズが大きいため、同じ処理でもより多くのスタック領域を消費する傾向があります。
このコミットは、`runtime.cgocallback_gofunc` のスタックフレームサイズを削減することで、このクリティカルパスにおけるスタック使用量を最適化し、128バイトの制限内に収めることを目的としています。これにより、プリエンプションとスタック分割の連携がより堅牢になり、ランタイムの安定性が向上します。
## 前提知識の解説
このコミットを理解するためには、以下のGoランタイムの概念とアセンブリの知識が必要です。
1. **Goランタイムのスケジューラ (M, P, G)**:
* **G (Goroutine)**: Goにおける軽量スレッド。Goプログラムの実行単位。
* **M (Machine)**: OSのスレッド。GoランタイムはMをOSに要求し、その上でGを実行します。
* **P (Processor)**: 論理プロセッサ。MとGを仲介し、Gの実行に必要なリソース(コンテキストなど)を提供します。MはPにアタッチされ、Pが持つ実行可能なGのキューからGを取り出して実行します。
2. **Cgo**: GoとC/C++コードを相互に呼び出すためのメカニズム。Cgoを介してC関数からGo関数が呼び出される場合、Goランタイムは特別な処理を行います。`cgocallback_gofunc` や `cgocallbackg` は、このCgoコールバックのパスで重要な役割を果たします。
3. **スタック管理**:
* **スタック分割 (Stack Split)**: Goのゴルーチンは、必要に応じてスタックを動的に拡張します。関数呼び出しの際に、スタックガードページに到達すると、ランタイムはより大きなスタックを割り当て、古いスタックの内容を新しいスタックにコピーします。これにより、初期スタックサイズを小さく保ち、メモリ使用量を削減します。
* **スタックガード (Stack Guard)**: スタックの境界を示す値。関数プロローグでスタックポインタがスタックガードを下回るかどうかをチェックし、下回る場合はスタック分割処理をトリガーします。
* **レッドゾーン (Red Zone)**: x86-64アーキテクチャにおいて、スタックポインタ (RSP) の直下128バイトの領域は、関数プロローグでスタックポインタを調整する前に一時的に使用できる領域として予約されています。この領域は、割り込みハンドラなどによって上書きされる可能性があるため、注意が必要です。Goランタイムは、このレッドゾーンを考慮してスタック使用量を管理し、割り込みなどによるデータ破損を防ぎます。
4. **プリエンプション (Preemption)**:
* Goランタイムは、長時間実行されるゴルーチンがCPUを独占するのを防ぐために、プリエンプションメカニズムを使用します。これにより、すべてのゴルーチンが公平にCPU時間を共有できます。
* プリエンプションは、通常、関数呼び出しのスタック分割チェックのタイミングで行われます。しかし、システムコールからの復帰など、特定のクリティカルなパスでは、プリエンプションを避ける必要があります。
5. **アセンブリ言語 (x86-32, x86-64, ARM)**:
* Goランタイムの低レベルな処理は、アセンブリ言語で記述されています。スタックフレームの操作、レジスタの使用、関数呼び出し規約などは、アセンブリコードを理解する上で不可欠です。
* `TEXT runtime·cgocallback_gofunc(SB),7,$12-12` のような表記は、Goのアセンブリ構文で、関数名、フラグ、フレームサイズ、引数サイズを示します。`$12-12` は、フレームサイズが12バイトで、引数サイズも12バイトであることを意味します。
6. **`reflect.call`**: Goの `reflect` パッケージの一部で、リフレクションを使って任意の関数を呼び出すためのメカニズムです。`cgocallbackg` は、CgoコールバックでGo関数を呼び出す際にこれを使用します。
7. **SEH (Structured Exception Handling)**: Windows固有の例外処理メカニズム。Goランタイムは、Windows上でCgoを使用する際に、SEHフレームを適切に設定する必要があります。
## 技術的詳細
このコミットの主要な目的は、`runtime.cgocallback_gofunc` のスタックフレームサイズを削減し、特にamd64アーキテクチャにおいて、プリエンプションとスタック分割の制約下でクリティカルパスのスタック使用量を128バイトのレッドゾーン内に収めることです。
具体的な変更点は以下の通りです。
1. **`runtime.cgocallback_gofunc` のスタックフレームサイズ削減**:
* **amd64 (`src/pkg/runtime/asm_amd64.s`)**:
* 変更前: `TEXT runtime·cgocallback_gofunc(SB),7,$24-24` (フレームサイズ24バイト)
* 変更後: `TEXT runtime·cgocallback_gofunc(SB),7,$16-24` (フレームサイズ16バイト)
* これにより、8バイトのスタック領域が解放されます。
* **x86-32 (`src/pkg/runtime/asm_386.s`)**:
* 変更前: `TEXT runtime·cgocallback_gofunc(SB),7,$12-12` (フレームサイズ12バイト)
* 変更後: `TEXT runtime·cgocallback_gofunc(SB),7,$8-12` (フレームサイズ8バイト)
* これにより、4バイトのスタック領域が解放されます。
* **ARM (`src/pkg/runtime/asm_arm.s`)**:
* 変更前: `TEXT runtime·cgocallback_gofunc(SB),7,$12-12` (フレームサイズ12バイト)
* 変更後: `TEXT runtime·cgocallback_gofunc(SB),7,$8-12` (フレームサイズ8バイト)
* これにより、4バイトのスタック領域が解放されます。
2. **`cgocallbackg` への引数渡し方法の変更**:
* 変更前は、`runtime.cgocallback_gofunc` から `runtime.cgocallbackg` を呼び出す際に、引数 (`fn`, `arg`, `argsize`) をスタックにプッシュしていました。
* 変更後は、これらの引数をスタックに直接プッシュするのではなく、`m->g0->sched.sp` を基点としたオフセットでアクセスするように変更されました。これは、`cgocall.c` で `CallbackArgs` 構造体が定義され、`CBARGS` マクロを使ってアクセスされることで実現されています。
* この変更により、`runtime.cgocallback_gofunc` のスタックフレーム内で引数を保持する必要がなくなり、スタック使用量が削減されます。
3. **スタックポインタの操作の最適化**:
* アセンブリコードにおいて、スタックポインタ (SP) の操作がより効率的になりました。例えば、`PUSHQ` や `POPQ` 命令の代わりに `MOVQ` 命令を使ってスタック上の値を操作したり、`LEAQ` 命令を使ってスタックポインタを調整したりすることで、命令数とスタック使用量を削減しています。
* 特に、`gobuf.pc` (ゴルーチンの実行再開アドレス) の保存方法が変更され、スタックに直接プッシュするのではなく、より効率的な方法で保存されるようになりました。
4. **`runtime.cgocallbackg` のシグネチャ変更**:
* 変更前: `runtime·cgocallbackg(FuncVal *fn, void *arg, uintptr argsize)`
* 変更後: `runtime·cgocallbackg(void)`
* 引数を直接受け取るのではなく、`CBARGS` マクロを通じて `m->g0->sched.sp` から引数を取得するように変更されました。これにより、`cgocallbackg` 自体のスタックフレームも簡素化されます。
5. **`unwindm` の修正**:
* `unwindm` 関数は、スタックをアンワインドする際に `m->g0->sched.sp` を復元する役割を担っています。
* x86-32 (`thechar == '5'`) のケースで、`m->g0->sched.sp` の復元ロジックが修正され、正しいオフセットから値を取得するように変更されました。これは、スタックフレームの変更に伴う調整です。
6. **Windows SEH (Structured Exception Handling) の調整**:
* `src/pkg/runtime/proc.c` の `runtime·needm` 関数において、WindowsのSEHフレームの設定ロジックが変更されました。
* 変更前は、`needm` の引数 `x` のアドレスを基点としてSEHフレームを設定していましたが、変更後は `cgocallback_gofunc` が `m->curg->sched.sp` の下に残す未使用のワードを利用するように変更されました。これにより、SEHフレームの配置がより適切になります。
これらの変更により、`runtime.cgocallback_gofunc` から `runtime.cgocallbackg` への呼び出しパスにおけるスタック使用量が削減され、特にamd64アーキテクチャにおいて、プリエンプションとスタック分割の制約下でのランタイムの安定性が向上します。
## コアとなるコードの変更箇所
このコミットでは、主に以下のファイルが変更されています。
* `src/pkg/runtime/asm_386.s`: x86-32アーキテクチャ向けのアセンブリコード。`runtime.cgocallback_gofunc` のスタックフレームサイズとスタック操作が変更されました。
* `src/pkg/runtime/asm_amd64.s`: amd64アーキテクチャ向けのアセンブリコード。`runtime.cgocallback_gofunc` のスタックフレームサイズとスタック操作が変更されました。
* `src/pkg/runtime/asm_arm.s`: ARMアーキテクチャ向けのアセンブリコード。`runtime.cgocallback_gofunc` のスタックフレームサイズとスタック操作が変更されました。
* `src/pkg/runtime/cgocall.c`: Cgoコールバック関連のCコード。`runtime.cgocallbackg` のシグネチャが変更され、引数へのアクセス方法が `CallbackArgs` 構造体と `CBARGS` マクロを通じて行われるようになりました。また、`unwindm` 関数も修正されました。
* `src/pkg/runtime/proc.c`: プロセス管理関連のCコード。WindowsにおけるSEHフレームの設定ロジックが変更されました。
## コアとなるコードの解説
### `src/pkg/runtime/asm_amd64.s` (amd64アーキテクチャの例)
```assembly
// 変更前
TEXT runtime·cgocallback_gofunc(SB),7,$24-24
// 変更後
TEXT runtime·cgocallback_gofunc(SB),7,$16-24
TEXT
ディレクティブの第3引数がスタックフレームサイズを示します。$24-24
から $16-24
に変更され、フレームサイズが24バイトから16バイトに削減されました。これにより、8バイトのスタック領域が節約されます。
// 変更前
PUSHQ $0
JMP needm
// 変更後
MOVL $0, BP
CMPQ CX, $0
JNE 2(PC)
needm
を呼び出す前のスタック操作が変更されました。PUSHQ $0
が削除され、MOVL $0, BP
と CMPQ CX, $0; JNE 2(PC)
に置き換えられています。これは、スタックに値をプッシュする代わりにレジスタを使用することで、スタック使用量を削減しています。
// 変更前
PUSHQ (g_sched+gobuf_sp)(SI)
// 変更後
MOVQ (g_sched+gobuf_sp)(SI), AX
MOVQ AX, 0(SP)
m->g0->sched.sp
の保存方法が変更されました。以前は直接スタックにプッシュしていましたが、レジスタ AX
を介して 0(SP)
に移動することで、より明示的なスタック操作を行っています。コメント NOTE: unwindm knows that the saved g->sched.sp is at 0(SP).
が追加され、unwindm
がこの変更を認識していることが示されています。
// 変更前
MOVQ fn+0(FP), AX
MOVQ frame+8(FP), BX
MOVQ framesize+16(FP), DX
// Push gobuf.pc
SUBQ $8, DI
MOVQ BP, 0(DI)
// Push arguments to cgocallbackg.
// Frame size here must match the frame size above plus the pushes
// to trick traceback routines into doing the right thing.
SUBQ $40, DI
MOVQ AX, 0(DI)
MOVQ BX, 8(DI)
MOVQ DX, 16(DI)
// Switch stack and make the call.
MOVQ DI, SP
CALL runtime·cgocallbackg(SB)
// 変更後
//
// In the new goroutine, 0(SP) and 8(SP) are unused except
// on Windows, where they are the SEH block.
MOVQ m_curg(BP), SI
MOVQ SI, g(CX)
MOVQ (g_sched+gobuf_sp)(SI), DI // prepare stack as DI
MOVQ (g_sched+gobuf_pc)(SI), BP
MOVQ BP, -8(DI)
LEAQ -(8+16)(DI), SP
CALL runtime·cgocallbackg(SB)
runtime.cgocallbackg
への引数渡しとスタック切り替えのロジックが大幅に変更されました。
- 変更前は、
fn
,frame
,framesize
をレジスタにロードし、それらをスタックにプッシュしてからcgocallbackg
を呼び出していました。スタックにgobuf.pc
と引数をプッシュするために合計48バイト (8 + 40
) を使用していました。 - 変更後は、引数をスタックに直接プッシュするのではなく、
m->curg->sched.sp
を基点としたオフセットでアクセスするように変更されました。gobuf.pc
は-8(DI)
に保存され、スタックポインタはLEAQ -(8+16)(DI), SP
で調整されます。これにより、スタック使用量が大幅に削減されます。コメントIn the new goroutine, 0(SP) and 8(SP) are unused except on Windows, where they are the SEH block.
が追加され、WindowsにおけるSEHブロックの配置に関する情報が提供されています。
// 変更前
MOVL 20(SP), BP
LEAL (20+4)(SP), DI
// 変更後
MOVL 8(SP), BP
LEAL (8+4)(SP), DI
g->sched
の復元ロジックも、スタックフレームサイズの変更に合わせてオフセットが調整されました。
// 変更前
POPL (g_sched+gobuf_sp)(SI)
// 変更後
MOVL 0(SP), AX
MOVL AX, (g_sched+gobuf_sp)(SI)
m->g0->sched.sp
の復元方法も変更されました。POPL
の代わりに MOV
命令を使用し、0(SP)
から値を取得するように変更されています。
// 変更前
POPL BP
// 変更後
MOVL 8(SP), BP
dropm
を呼び出す前の BP
レジスタの復元方法も変更されました。POPL
の代わりに MOV
命令を使用し、8(SP)
から値を取得するように変更されています。
src/pkg/runtime/cgocall.c
// 変更前
void
runtime·cgocallbackg(FuncVal *fn, void *arg, uintptr argsize)
// 変更後
typedef struct CallbackArgs CallbackArgs;
struct CallbackArgs
{
FuncVal *fn;
void *arg;
uintptr argsize;
};
#define CBARGS (CallbackArgs*)((byte*)m->g0->sched.sp+(3+(thechar=='5'))*sizeof(void*))
void
runtime·cgocallbackg(void)
runtime.cgocallbackg
のシグネチャが変更され、引数を直接受け取らないようになりました。代わりに、CallbackArgs
構造体と CBARGS
マクロが導入され、m->g0->sched.sp
を基点としてスタック上の引数にアクセスするように変更されました。CBARGS
マクロは、アーキテクチャ (thechar
) に応じて適切なオフセットを計算します。
// 変更前
reflect·call(fn, arg, argsize);
// 変更後
cb = CBARGS;
reflect·call(cb->fn, cb->arg, cb->argsize);
reflect·call
の呼び出し箇所で、fn
, arg
, argsize
を直接使用する代わりに、CBARGS
マクロで取得した CallbackArgs
構造体のメンバーを使用するように変更されました。
// 変更前
case '5':
m->g0->sched.sp = *(uintptr*)m->g0->sched.sp;
break;
// 変更後
case '5':
m->g0->sched.sp = *(uintptr*)((byte*)m->g0->sched.sp + 4);
break;
unwindm
関数におけるx86-32 (thechar == '5'
) のケースで、m->g0->sched.sp
の復元ロジックが修正されました。スタックフレームの変更に伴い、正しいオフセット (+4
) から値を取得するように調整されています。
src/pkg/runtime/proc.c
// 変更前
// On windows/386, we need to put an SEH frame (two words)
// somewhere on the current stack. We are called
// from needm, and we know there is some available
// space one word into the argument frame. Use that.
m->seh = (SEH*)((uintptr*)&x + 1);
// 変更後
// On windows/386, we need to put an SEH frame (two words)
// somewhere on the current stack. We are called from cgocallback_gofunc
// and we know that it will leave two unused words below m->curg->sched.sp.
// Use those.
m->seh = (SEH*)((uintptr*)m->curg->sched.sp - 3);
WindowsにおけるSEHフレームの設定ロジックが変更されました。以前は needm
の引数 x
のアドレスを基点としていましたが、cgocallback_gofunc
が m->curg->sched.sp
の下に残す未使用のワードを利用するように変更されました。これにより、SEHフレームの配置がより適切になり、スタック使用量の最適化に貢献します。
関連リンク
- Goのスタック管理に関する公式ドキュメントや設計ドキュメントは、Goのソースコードリポジトリ内の
src/runtime/
ディレクトリや、Goの設計ドキュメント(例: Go 1.2 Stack Split)に詳細があります。 - Cgoに関する公式ドキュメント: https://pkg.go.dev/cmd/cgo
- Goのスケジューラに関する詳細な解説: https://go.dev/doc/articles/go1.20 (Go 1.20のリリースノートですが、スケジューラに関する基本的な概念が説明されています)
参考にした情報源リンク
- Goのソースコード (特に
src/runtime/
ディレクトリのアセンブリファイルとCファイル) - Goの公式ドキュメント
- Goの設計ドキュメント (Go 1.2 Stack Splitなど)
- x86-64アーキテクチャの呼び出し規約とレッドゾーンに関する一般的な情報 (例: System V Application Binary Interface)
- Windows Structured Exception Handling (SEH) に関するMicrosoftのドキュメント