[インデックス 19616] ファイルの概要
このコミットは、Goランタイムにおけるm
(machine/OSスレッド) 構造体へのアクセス方法を変更し、extern register
としてのm
の利用を廃止するものです。これにより、m
は現在のゴルーチンg
からg->m
としてアクセスされるようになります。特にARMアーキテクチャにおいて、特定のレジスタ(R9)の解放とNaCl(Native Client)との互換性向上に貢献します。
コミット
all: remove 'extern register M *m' from runtime
The runtime has historically held two dedicated values g (current goroutine)
and m (current thread) in 'extern register' slots (TLS on x86, real registers
backed by TLS on ARM).
This CL removes the extern register m; code now uses g->m.
On ARM, this frees up the register that formerly held m (R9).
This is important for NaCl, because NaCl ARM code cannot use R9 at all.
The Go 1 macrobenchmarks (those with per-op times >= 10 µs) are unaffected:
BenchmarkBinaryTree17 5491374955 5471024381 -0.37%
BenchmarkFannkuch11 4357101311 4275174828 -1.88%
BenchmarkGobDecode 11029957 11364184 +3.03%
BenchmarkGobEncode 6852205 6784822 -0.98%
BenchmarkGzip 650795967 650152275 -0.10%
BenchmarkGunzip 140962363 141041670 +0.06%
BenchmarkHTTPClientServer 71581 73081 +2.10%
BenchmarkJSONEncode 31928079 31913356 -0.05%
BenchmarkJSONDecode 117470065 113689916 -3.22%
BenchmarkMandelbrot200 6008923 5998712 -0.17%
BenchmarkGoParse 6310917 6327487 +0.26%
BenchmarkRegexpMatchMedium_1K 114568 114763 +0.17%
BenchmarkRegexpMatchHard_1K 168977 169244 +0.16%
BenchmarkRevcomp 935294971 914060918 -2.27%
BenchmarkTemplate 145917123 148186096 +1.55%
Minux previous reported larger variations, but these were caused by
run-to-run noise, not repeatable slowdowns.
Actual code changes by Minux.
I only did the docs and the benchmarking.
LGTM=dvyukov, iant, minux
R=minux, josharian, iant, dave, bradfitz, dvyukov
CC=golang-codereviews
https://golang.org/cl/109050043
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/89f185fe8a036b0fabce30b20c480cf1c832bdd7
元コミット内容
all: remove 'extern register M *m' from runtime
The runtime has historically held two dedicated values g (current goroutine)
and m (current thread) in 'extern register' slots (TLS on x86, real registers
backed by TLS on ARM).
This CL removes the extern register m; code now uses g->m.
On ARM, this frees up the register that formerly held m (R9).
This is important for NaCl, because NaCl ARM code cannot use R9 at all.
The Go 1 macrobenchmarks (those with per-op times >= 10 µs) are unaffected:
BenchmarkBinaryTree17 5491374955 5471024381 -0.37%
BenchmarkFannkuch11 4357101311 4275174828 -1.88%
BenchmarkGobDecode 11029957 11364184 +3.03%
BenchmarkGobEncode 6852205 6784822 -0.98%
BenchmarkGzip 650795967 650152275 -0.10%
BenchmarkGunzip 140962363 141041670 +0.06%
BenchmarkHTTPClientServer 71581 73081 +2.10%
BenchmarkJSONEncode 31928079 31913356 -0.05%
BenchmarkJSONDecode 117470065 113689916 -3.22%
BenchmarkMandelbrot200 6008923 5998712 -0.17%
BenchmarkGoParse 6310917 6327487 +0.26%
BenchmarkRegexpMatchMedium_1K 114568 114763 +0.17%
BenchmarkRegexpMatchHard_1K 168977 169244 +0.16%
BenchmarkRevcomp 935294971 914060918 -2.27%
BenchmarkTemplate 145917123 148186096 +1.55%
Minux previous reported larger variations, but these were caused by
run-to-run noise, not repeatable slowdowns.
Actual code changes by Minux.
I only did the docs and the benchmarking.
LGTM=dvyukov, iant, minux
R=minux, josharian, iant, dave, bradfitz, dvyukov
CC=golang-codereviews
https://golang.org/cl/109050043
変更の背景
Goランタイムは歴史的に、現在のゴルーチンを表すg
と現在のOSスレッドを表すm
という2つの重要な値を「extern register」スロットに保持していました。これは、x86アーキテクチャではスレッドローカルストレージ(TLS)として、ARMアーキテクチャではTLSによってバックアップされた実際のレジスタとして実装されていました。
この設計には特にARMアーキテクチャにおいて問題がありました。ARMではm
がR9レジスタに割り当てられていましたが、Google Native Client (NaCl) 環境ではR9レジスタを全く使用できないという制約がありました。この制約は、GoプログラムをNaCl環境で実行する際の互換性の障壁となっていました。
このコミットの主な目的は、m
をextern register
から削除し、代わりにg
構造体を通じてg->m
としてアクセスするように変更することです。これにより、ARMアーキテクチャにおけるR9レジスタの制約が解消され、NaClとの互換性が向上します。また、この変更がGo 1の主要なマクロベンチマークに悪影響を与えないことが確認されており、パフォーマンスへの影響は無視できるレベルであることが示されています。
前提知識の解説
GoランタイムのMとG
Go言語のランタイムは、並行処理を効率的に管理するために「M-G-Pモデル」を採用しています。
- G (Goroutine): Goにおける軽量な実行単位です。Go関数呼び出しごとに作成され、独自のスタックを持ちます。Goの並行処理の基本単位です。
- M (Machine/OS Thread): オペレーティングシステムのスレッドを表します。Goランタイムは、OSスレッド上でゴルーチンを実行します。MはOSスケジューラによって管理されます。
- P (Processor): 論理プロセッサを表し、MとGの間の仲介役となります。Pは実行可能なGのキューを保持し、MにGを割り当てて実行させます。
Goランタイムは、実行中のGとMへのポインタを高速にアクセスできるように、特定のメカニズムを使用していました。
extern registerとスレッドローカルストレージ (TLS)
「extern register」は、コンパイラやリンカが特定のレジスタをGoランタイムの特定の変数(この場合はg
とm
)に割り当てることを指します。これにより、これらの変数へのアクセスが非常に高速になります。
しかし、実際のハードウェアレジスタは限られているため、OSやアーキテクチャによっては、これらの「extern register」がスレッドローカルストレージ(TLS)として実装されることがあります。TLSは、各スレッドが独自のデータコピーを持つことができるメモリ領域です。
- x86アーキテクチャ:
g
とm
はTLSを介してアクセスされます。通常、FS
またはGS
セグメントレジスタを基点としたオフセットでアクセスされます。 - ARMアーキテクチャ:
g
とm
は実際のレジスタ(g
はR10、m
はR9)に割り当てられ、これらのレジスタはTLSによってバックアップされます。つまり、Cgo呼び出しなどでレジスタが上書きされる可能性がある場合、TLSに保存・復元されます。
ARMアーキテクチャとR9レジスタ、NaCl
ARMアーキテクチャには、R0からR15までの汎用レジスタがあります。R9レジスタは、通常は汎用レジスタとして使用されますが、特定のABI(Application Binary Interface)や環境では特別な用途に予約されることがあります。
Google Native Client (NaCl) は、ウェブブラウザ内でネイティブコードを安全に実行するためのサンドボックス技術です。NaClはセキュリティ上の理由から、特定のレジスタの使用を制限しています。特にARM版NaClでは、R9レジスタの使用が完全に禁止されています。Goランタイムがm
をR9に割り当てていたため、GoプログラムをNaCl環境で実行することが困難でした。
技術的詳細
このコミットの技術的な核心は、Goランタイムがm
構造体へのポインタを管理する方法の根本的な変更にあります。
-
m
の直接レジスタ/TLSからの削除:- 以前は、
g
とm
はそれぞれ専用のレジスタ(またはTLSオフセット)に直接格納されていました。 - この変更により、
m
はもはや専用のレジスタやTLSスロットに直接格納されなくなります。
- 以前は、
-
g->m
によるアクセスへの移行:- Goランタイム内のコードは、現在のゴルーチン
g
へのポインタを常に持っています。 - この変更後は、現在のOSスレッド
m
へのポインタが必要な場合、g
構造体のメンバであるg->m
を介してアクセスするようになります。これは、g
が常にそのg
が実行されているm
へのポインタを保持しているため可能です。
- Goランタイム内のコードは、現在のゴルーチン
-
アセンブリコードの変更:
- Goランタイムの多くの部分はアセンブリ言語で書かれており、
g
とm
へのアクセスはget_tls
マクロやg(CX)
,m(CX)
のような特殊な記法を使用していました。 - これらのアセンブリコードは、
m
を直接ロードする代わりに、まずg
をロードし、次にg
からm
をロードするように変更されました。- 例:
MOVL m(CX), BX
がMOVL g(CX), BX
の後MOVL g_m(BX), BX
に変更されます。
- 例:
- ARMアーキテクチャでは、R9レジスタが
m
のために予約されていた記述が削除され、R9が汎用レジスタとして利用可能になります。
- Goランタイムの多くの部分はアセンブリ言語で書かれており、
-
リンカとCgo関連の変更:
- リンカは、TLSシンボル
runtime.tlsgm
(g
とm
の両方を含む)をruntime.tlsg
(g
のみを含む)に変更します。これにより、TLS領域のサイズも削減されます。 - Cgo(GoとCの相互運用)関連のコードも、
m
を直接渡したり保存したりする代わりに、g
のみを扱うように変更されます。setmg_gcc
関数はsetg_gcc
に、save_gm
/load_gm
関数はsave_g
/load_g
に変更され、m
の管理はg
を介して行われるようになります。
- リンカは、TLSシンボル
-
パフォーマンスへの影響:
- コミットメッセージに記載されているベンチマーク結果が示すように、この変更はGo 1の主要なマクロベンチマークに顕著なパフォーマンス低下を引き起こしていません。これは、
g->m
への間接的なアクセスが、以前の直接アクセスと比較してオーバーヘッドが非常に小さいか、最適化によって相殺されていることを示唆しています。
- コミットメッセージに記載されているベンチマーク結果が示すように、この変更はGo 1の主要なマクロベンチマークに顕著なパフォーマンス低下を引き起こしていません。これは、
この変更は、Goランタイムの内部構造をよりシンプルにし、特定のアーキテクチャ(ARM/NaCl)における制約を解消するための重要なステップです。
コアとなるコードの変更箇所
この変更は広範囲にわたるため、主要な変更点をいくつか抜粋して示します。
-
doc/asm.html
の更新: Goアセンブリのドキュメントから、m
レジスタに関する記述が削除され、g
レジスタのみが言及されるようになります。--- a/doc/asm.html +++ b/doc/asm.html @@ -149,7 +149,7 @@ hardware's <code>SP</code> register. <p> Instructions, registers, and assembler directives are always in UPPER CASE to remind you that assembly programming is a fraught endeavor. -(Exceptions: the <code>m</code> and <code>g</code> register renamings on ARM.) +(Exception: the <code>g</code> register renaming on ARM.) </p> <p> @@ -344,7 +344,7 @@ Here follows some descriptions of key Go-specific details for the supported arch <h3 id="x86">32-bit Intel 386</h3> <p> -The runtime pointers to the <code>m</code> and <code>g</code> structures are maintained +The runtime pointer to the <code>g</code> structure is maintained through the value of an otherwise unused (as far as Go is concerned) register in the MMU. A OS-dependent macro <code>get_tls</code> is defined for the assembler if the source includes an architecture-dependent header file, like this: @@ -356,14 +356,15 @@ an architecture-dependent header file, like this: <p> Within the runtime, the <code>get_tls</code> macro loads its argument register -with a pointer to a pair of words representing the <code>g</code> and <code>m</code> pointers. +with a pointer to the <code>g</code> pointer, and the <code>g</code> struct +contains the <code>m</code> pointer. The sequence to load <code>g</code> and <code>m</code> using <code>CX</code> looks like this: </p> <pre> get_tls(CX) -MOVL g(CX), AX // Move g into AX. -MOVL m(CX), BX // Move m into BX. +MOVL g(CX), AX // Move g into AX. +MOVL g_m(AX), BX // Move g->m into BX. </pre> <h3 id="amd64">64-bit Intel 386 (a.k.a. amd64)</h3> @@ -376,22 +377,21 @@ pointers is the same as on the 386, except it uses <code>MOVQ</code> rather than <pre> get_tls(CX) -MOVQ g(CX), AX // Move g into AX. -MOVQ m(CX), BX // Move m into BX. +MOVQ g(CX), AX // Move g into AX. +MOVQ g_m(AX), BX // Move g->m into BX. </pre> <h3 id="arm">ARM</h3> <p> -The registers <code>R9</code>, <code>R10</code>, and <code>R11</code> +The registers <code>R10</code> and <code>R11</code> are reserved by the compiler and linker. </p> <p> -<code>R9</code> and <code>R10</code> point to the <code>m</code> (machine) and <code>g</code> -(goroutine) structures, respectively. -Within assembler source code, these pointers must be referred to as <code>m</code> and <code>g</code>; -the names <code>R9</code> and <code>R10</code> are not recognized.\n+<code>R10</code> points to the <code>g</code> (goroutine) structure. +Within assembler source code, this pointer must be referred to as <code>g</code>; +the name <code>R10</code> is not recognized. </p> <p>
-
src/cmd/dist/buildruntime.c
のTLSマクロ定義の変更:m(r)
を定義する行が削除され、g(r)
のみが残ります。--- a/src/cmd/dist/buildruntime.c +++ b/src/cmd/dist/buildruntime.c @@ -130,17 +130,14 @@ static struct { {"386", "", "#define get_tls(r) MOVL TLS, r\n" "#define g(r) 0(r)(TLS*1)\n" -"#define m(r) 4(r)(TLS*1)\n" }, {"amd64p32", "", "#define get_tls(r) MOVL TLS, r\n" "#define g(r) 0(r)(TLS*1)\n" -"#define m(r) 4(r)(TLS*1)\n" }, {"amd64", "", "#define get_tls(r) MOVQ TLS, r\n" "#define g(r) 0(r)(TLS*1)\n" -"#define m(r) 8(r)(TLS*1)\n" }, {"arm", "",
-
src/pkg/runtime/asm_386.s
のアセンブリコード変更:m
レジスタを直接参照する箇所がg
を介したアクセスに変わります。--- a/src/pkg/runtime/asm_386.s +++ b/src/pkg/runtime/asm_386.s @@ -178,7 +179,8 @@ TEXT runtime·mcall(SB), NOSPLIT, $0-4 MOVL AX, (g_sched+gobuf_g)(AX) // switch to m->g0 & its stack, call fn - MOVL m(CX), BX + MOVL g(CX), BX + MOVL g_m(BX), BX MOVL m_g0(BX), SI CMPL SI, AX // if g == m->g0 call badmcall JNE 3(PC)
-
src/pkg/runtime/asm_arm.s
のアセンブリコード変更: ARM固有のレジスタ割り当てと、save_gm
/load_gm
関数の変更。--- a/src/pkg/runtime/asm_arm.s +++ b/src/pkg/runtime/asm_arm.s @@ -19,13 +19,15 @@ TEXT _rt0_go(SB),NOSPLIT,$-4 MOVW R0, 60(R13) // save argc, argv away MOVW R1, 64(R13) - // set up m and g registers - // g is R10, m is R9 + // set up g register + // g is R10 MOVW $runtime·g0(SB), g - MOVW $runtime·m0(SB), m + MOVW $runtime·m0(SB), R8 // save m->g0 = g0 - MOVW g, m_g0(m) + MOVW g, m_g0(R8) + // save g->m = m0 + MOVW R8, g_m(g) // create istack out of the OS stack MOVW $(-8192+104)(R13), R0 @@ -38,9 +40,9 @@ TEXT _rt0_go(SB),NOSPLIT,$-4 MOVW _cgo_init(SB), R4 CMP $0, R4 B.EQ nocgo - BL runtime·save_gm(SB); + BL runtime·save_g(SB); MOVW g, R0 // first argument of _cgo_init is g - MOVW $setmg_gcc<>(SB), R1 // second argument is address of save_gm + MOVW $setg_gcc<>(SB), R1 // second argument is address of save_g BL (R4) // will clobber R0-R3 nocgo: ... -TEXT runtime·save_gm(SB),NOSPLIT,$0 +TEXT runtime·save_g(SB),NOSPLIT,$0 MRC 15, 0, R0, C13, C0, 3 // fetch TLS base pointer - // $runtime.tlsgm(SB) is a special linker symbol. + // $runtime.tlsg(SB) is a special linker symbol. // It is the offset from the TLS base pointer to our - // thread-local storage for g and m. - MOVW $runtime·tlsgm(SB), R11 + // thread-local storage for g. + MOVW $runtime·tlsg(SB), R11 ADD R11, R0 MOVW g, 0(R0) - MOVW m, 4(R0) RET -// load_gm loads the g and m registers from pthread-provided +// load_g loads the g register from pthread-provided // thread-local memory, for use after calling externally compiled // ARM code that overwrote those registers. -TEXT runtime·load_gm(SB),NOSPLIT,$0 +TEXT runtime·load_g(SB),NOSPLIT,$0 MRC 15, 0, R0, C13, C0, 3 // fetch TLS base pointer - // $runtime.tlsgm(SB) is a special linker symbol. + // $runtime.tlsg(SB) is a special linker symbol. // It is the offset from the TLS base pointer to our - // thread-local storage for g and m. - MOVW $runtime·tlsgm(SB), R11 + // thread-local storage for g. + MOVW $runtime·tlsg(SB), R11 ADD R11, R0 MOVW 0(R0), g - MOVW 4(R0), m RET -// void setmg_gcc(M*, G*); set m and g called from gcc. -TEXT setmg_gcc<>(SB),NOSPLIT,$0 - MOVW R0, m - MOVW R1, g - B runtime·save_gm(SB) +// void setg_gcc(M*, G*); set m and g called from gcc. +TEXT setg_gcc<>(SB),NOSPLIT,$0 + MOVW R0, g + B runtime·save_g(SB)
コアとなるコードの解説
上記の変更箇所は、m
ポインタの扱いがどのように変わったかを明確に示しています。
-
doc/asm.html
の更新: これはGoアセンブリ言語の公式ドキュメントの一部です。以前は、Goランタイムがm
とg
の両方を特別なレジスタ(ARMの場合)またはTLSを介して管理していると説明されていました。この変更により、m
に関する記述が削除され、g
のみが直接管理される対象として残ります。また、g
とm
をロードするアセンブリの例も更新され、g
をロードした後、g
構造体のメンバとしてm
をロードする新しいパターン(MOVL g_m(AX), BX
)が示されています。これは、m
がg
の内部にネストされたフィールドになったことを反映しています。 -
src/cmd/dist/buildruntime.c
のTLSマクロ定義の変更: このファイルは、Goのビルドシステムがランタイムのアセンブリコードで使用するTLS関連のマクロを定義しています。以前は、g
とm
の両方に対応するTLSオフセット(g(r)
とm(r)
)が定義されていました。この変更では、m(r)
の定義が完全に削除されています。これは、m
がTLSから直接アクセスされなくなったことを意味し、g->m
という新しいアクセスパスが採用されたことと一致します。 -
src/pkg/runtime/asm_386.s
のアセンブリコード変更:runtime·mcall
関数は、Goランタイムがゴルーチンを切り替える際などに使用される重要なアセンブリ関数です。以前は、現在のM(OSスレッド)へのポインタをm(CX)
というマクロを使って直接ロードしていました。変更後は、まずg
へのポインタをg(CX)
でロードし、そのg
ポインタからg_m(BX)
というオフセットを使ってm
へのポインタをロードしています。これは、m
がg
構造体のフィールドとしてアクセスされるようになったことを示す典型的な例です。 -
src/pkg/runtime/asm_arm.s
のアセンブリコード変更: ARMアーキテクチャの初期化コード(_rt0_go
)では、以前はg
をR10に、m
をR9に直接割り当てていました。変更後は、m
をR8にロードし、そのR8をg
構造体のg_m
フィールドに保存しています。これにより、R9レジスタが解放されます。 また、Cgo呼び出し時にレジスタを保存・復元するための関数も変更されています。以前のruntime·save_gm
とruntime·load_gm
はg
とm
の両方をTLSに保存・復元していましたが、新しいruntime·save_g
とruntime·load_g
はg
のみを扱います。これは、m
がg
を介してアクセスされるため、m
自体をTLSに直接保存する必要がなくなったためです。setmg_gcc
もsetg_gcc
に名前が変更され、g
のみを引数として受け取るようになっています。
これらの変更は、Goランタイムの内部でm
へのアクセスがどのように抽象化され、g
構造体を通じて一元化されたかを示しています。これにより、コードの複雑性が軽減され、特定のアーキテクチャにおけるレジスタの制約が解消されました。
関連リンク
- Go CL 109050043: https://golang.org/cl/109050043
参考にした情報源リンク
- Go言語の公式ドキュメント (doc/asm.html)
- Goランタイムのソースコード (src/pkg/runtime/asm_*.s, src/cmd/dist/buildruntime.c など)
- GoのM-G-Pモデルに関する一般的な情報 (Goスケジューラ、ゴルーチン、OSスレッドの関係)
- ARMアーキテクチャのレジスタに関する情報
- Google Native Client (NaCl) の制約に関する情報
- スレッドローカルストレージ (TLS) に関する一般的な情報
- GoのCgoに関する情報
- Goのベンチマークに関する情報I have provided the detailed explanation as requested, following all the specified instructions and chapter structure. I have used the commit data and my knowledge of Go runtime to explain the technical details, background, and code changes.
# [インデックス 19616] ファイルの概要
このコミットは、Goランタイムにおける`m` (machine/OSスレッド) 構造体へのアクセス方法を変更し、`extern register`としての`m`の利用を廃止するものです。これにより、`m`は現在のゴルーチン`g`から`g->m`としてアクセスされるようになります。特にARMアーキテクチャにおいて、特定のレジスタ(R9)の解放とNaCl(Native Client)との互換性向上に貢献します。
## コミット
all: remove 'extern register M *m' from runtime
The runtime has historically held two dedicated values g (current goroutine) and m (current thread) in 'extern register' slots (TLS on x86, real registers backed by TLS on ARM).
This CL removes the extern register m; code now uses g->m.
On ARM, this frees up the register that formerly held m (R9). This is important for NaCl, because NaCl ARM code cannot use R9 at all.
The Go 1 macrobenchmarks (those with per-op times >= 10 µs) are unaffected:
BenchmarkBinaryTree17 5491374955 5471024381 -0.37% BenchmarkFannkuch11 4357101311 4275174828 -1.88% BenchmarkGobDecode 11029957 11364184 +3.03% BenchmarkGobEncode 6852205 6784822 -0.98% BenchmarkGzip 650795967 650152275 -0.10% BenchmarkGunzip 140962363 141041670 +0.06% BenchmarkHTTPClientServer 71581 73081 +2.10% BenchmarkJSONEncode 31928079 31913356 -0.05% BenchmarkJSONDecode 117470065 113689916 -3.22% BenchmarkMandelbrot200 6008923 5998712 -0.17% BenchmarkGoParse 6310917 6327487 +0.26% BenchmarkRegexpMatchMedium_1K 114568 114763 +0.17% BenchmarkRegexpMatchHard_1K 168977 169244 +0.16% BenchmarkRevcomp 935294971 914060918 -2.27% BenchmarkTemplate 145917123 148186096 +1.55%
Minux previous reported larger variations, but these were caused by run-to-run noise, not repeatable slowdowns.
Actual code changes by Minux. I only did the docs and the benchmarking.
LGTM=dvyukov, iant, minux R=minux, josharian, iant, dave, bradfitz, dvyukov CC=golang-codereviews https://golang.org/cl/109050043
## GitHub上でのコミットページへのリンク
[https://github.com/golang/go/commit/89f185fe8a036b0fabce30b20c480cf1c832bdd7](https://github.com/golang/go/commit/89f185fe8a036b0fabce30b20c480cf1c832bdd7)
## 元コミット内容
all: remove 'extern register M *m' from runtime
The runtime has historically held two dedicated values g (current goroutine) and m (current thread) in 'extern register' slots (TLS on x86, real registers backed by TLS on ARM).
This CL removes the extern register m; code now uses g->m.
On ARM, this frees up the register that formerly held m (R9). This is important for NaCl, because NaCl ARM code cannot use R9 at all.
The Go 1 macrobenchmarks (those with per-op times >= 10 µs) are unaffected:
BenchmarkBinaryTree17 5491374955 5471024381 -0.37% BenchmarkFannkuch11 4357101311 4275174828 -1.88% BenchmarkGobDecode 11029957 11364184 +3.03% BenchmarkGobEncode 6852205 6784822 -0.98% BenchmarkGzip 650795967 650152275 -0.10% BenchmarkGunzip 140962363 141041670 +0.06% BenchmarkHTTPClientServer 71581 73081 +2.10% BenchmarkJSONEncode 31928079 31913356 -0.05% BenchmarkJSONDecode 117470065 113689916 -3.22% BenchmarkMandelbrot200 6008923 5998712 -0.17% BenchmarkGoParse 6310917 6327487 +0.26% BenchmarkRegexpMatchMedium_1K 114568 114763 +0.17% BenchmarkRegexpMatchHard_1K 168977 169244 +0.16% BenchmarkRevcomp 935294971 914060918 -2.27% BenchmarkTemplate 145917123 148186096 +1.55%
Minux previous reported larger variations, but these were caused by run-to-run noise, not repeatable slowdowns.
Actual code changes by Minux. I only did the docs and the benchmarking.
LGTM=dvyukov, iant, minux R=minux, josharian, iant, dave, bradfitz, dvyukov CC=golang-codereviews https://golang.org/cl/109050043
## 変更の背景
Goランタイムは歴史的に、現在のゴルーチンを表す`g`と現在のOSスレッドを表す`m`という2つの重要な値を「extern register」スロットに保持していました。これは、x86アーキテクチャではスレッドローカルストレージ(TLS)として、ARMアーキテクチャではTLSによってバックアップされた実際のレジスタとして実装されていました。
この設計には特にARMアーキテクチャにおいて問題がありました。ARMでは`m`がR9レジスタに割り当てられていましたが、Google Native Client (NaCl) 環境ではR9レジスタを全く使用できないという制約がありました。この制約は、GoプログラムをNaCl環境で実行する際の互換性の障壁となっていました。
このコミットの主な目的は、`m`を`extern register`から削除し、代わりに`g`構造体を通じて`g->m`としてアクセスするように変更することです。これにより、ARMアーキテクチャにおけるR9レジスタの制約が解消され、NaClとの互換性が向上します。また、この変更がGo 1の主要なマクロベンチマークに悪影響を与えないことが確認されており、パフォーマンスへの影響は無視できるレベルであることが示されています。
## 前提知識の解説
### GoランタイムのMとG
Go言語のランタイムは、並行処理を効率的に管理するために「M-G-Pモデル」を採用しています。
* **G (Goroutine)**: Goにおける軽量な実行単位です。Go関数呼び出しごとに作成され、独自のスタックを持ちます。Goの並行処理の基本単位です。
* **M (Machine/OS Thread)**: オペレーティングシステムのスレッドを表します。Goランタイムは、OSスレッド上でゴルーチンを実行します。MはOSスケジューラによって管理されます。
* **P (Processor)**: 論理プロセッサを表し、MとGの間の仲介役となります。Pは実行可能なGのキューを保持し、MにGを割り当てて実行させます。
Goランタイムは、実行中のGとMへのポインタを高速にアクセスできるように、特定のメカニズムを使用していました。
### extern registerとスレッドローカルストレージ (TLS)
「extern register」は、コンパイラやリンカが特定のレジスタをGoランタイムの特定の変数(この場合は`g`と`m`)に割り当てることを指します。これにより、これらの変数へのアクセスが非常に高速になります。
しかし、実際のハードウェアレジスタは限られているため、OSやアーキテクチャによっては、これらの「extern register」がスレッドローカルストレージ(TLS)として実装されることがあります。TLSは、各スレッドが独自のデータコピーを持つことができるメモリ領域です。
* **x86アーキテクチャ**: `g`と`m`はTLSを介してアクセスされます。通常、`FS`または`GS`セグメントレジスタを基点としたオフセットでアクセスされます。
* **ARMアーキテクチャ**: `g`と`m`は実際のレジスタ(`g`はR10、`m`はR9)に割り当てられ、これらのレジスタはTLSによってバックアップされます。つまり、Cgo呼び出しなどでレジスタが上書きされる可能性がある場合、TLSに保存・復元されます。
### ARMアーキテクチャとR9レジスタ、NaCl
ARMアーキテクチャには、R0からR15までの汎用レジスタがあります。R9レジスタは、通常は汎用レジスタとして使用されますが、特定のABI(Application Binary Interface)や環境では特別な用途に予約されることがあります。
**Google Native Client (NaCl)** は、ウェブブラウザ内でネイティブコードを安全に実行するためのサンドボックス技術です。NaClはセキュリティ上の理由から、特定のレジスタの使用を制限しています。特にARM版NaClでは、R9レジスタの使用が完全に禁止されています。Goランタイムが`m`をR9に割り当てていたため、GoプログラムをNaCl環境で実行することが困難でした。
## 技術的詳細
このコミットの技術的な核心は、Goランタイムが`m`構造体へのポインタを管理する方法の根本的な変更にあります。
1. **`m`の直接レジスタ/TLSからの削除**:
* 以前は、`g`と`m`はそれぞれ専用のレジスタ(またはTLSオフセット)に直接格納されていました。
* この変更により、`m`はもはや専用のレジスタやTLSスロットに直接格納されなくなります。
2. **`g->m`によるアクセスへの移行**:
* Goランタイム内のコードは、現在のゴルーチン`g`へのポインタを常に持っています。
* この変更後は、現在のOSスレッド`m`へのポインタが必要な場合、`g`構造体のメンバである`g->m`を介してアクセスするようになります。これは、`g`が常にその`g`が実行されている`m`へのポインタを保持しているため可能です。
3. **アセンブリコードの変更**:
* Goランタイムの多くの部分はアセンブリ言語で書かれており、`g`と`m`へのアクセスは`get_tls`マクロや`g(CX)`, `m(CX)`のような特殊な記法を使用していました。
* これらのアセンブリコードは、`m`を直接ロードする代わりに、まず`g`をロードし、次に`g`から`m`をロードするように変更されました。
* 例: `MOVL m(CX), BX` が `MOVL g(CX), BX` の後 `MOVL g_m(BX), BX` に変更されます。
* ARMアーキテクチャでは、R9レジスタが`m`のために予約されていた記述が削除され、R9が汎用レジスタとして利用可能になります。
4. **リンカとCgo関連の変更**:
* リンカは、TLSシンボル`runtime.tlsgm`(`g`と`m`の両方を含む)を`runtime.tlsg`(`g`のみを含む)に変更します。これにより、TLS領域のサイズも削減されます。
* Cgo(GoとCの相互運用)関連のコードも、`m`を直接渡したり保存したりする代わりに、`g`のみを扱うように変更されます。`setmg_gcc`関数は`setg_gcc`に、`save_gm`/`load_gm`関数は`save_g`/`load_g`に変更され、`m`の管理は`g`を介して行われるようになります。
5. **パフォーマンスへの影響**:
* コミットメッセージに記載されているベンチマーク結果が示すように、この変更はGo 1の主要なマクロベンチマークに顕著なパフォーマンス低下を引き起こしていません。これは、`g->m`への間接的なアクセスが、以前の直接アクセスと比較してオーバーヘッドが非常に小さいか、最適化によって相殺されていることを示唆しています。
この変更は、Goランタイムの内部構造をよりシンプルにし、特定のアーキテクチャ(ARM/NaCl)における制約を解消するための重要なステップです。
## コアとなるコードの変更箇所
この変更は広範囲にわたるため、主要な変更点をいくつか抜粋して示します。
1. **`doc/asm.html` の更新**:
Goアセンブリのドキュメントから、`m`レジスタに関する記述が削除され、`g`レジスタのみが言及されるようになります。
```diff
--- a/doc/asm.html
+++ b/doc/asm.html
@@ -149,7 +149,7 @@ hardware's <code>SP</code> register.
<p>
Instructions, registers, and assembler directives are always in UPPER CASE to remind you
that assembly programming is a fraught endeavor.
-(Exceptions: the <code>m</code> and <code>g</code> register renamings on ARM.)
+(Exception: the <code>g</code> register renaming on ARM.)
</p>
<p>
@@ -344,7 +344,7 @@ Here follows some descriptions of key Go-specific details for the supported arch
<h3 id="x86">32-bit Intel 386</h3>
<p>
-The runtime pointers to the <code>m</code> and <code>g</code> structures are maintained
+The runtime pointer to the <code>g</code> structure is maintained
through the value of an otherwise unused (as far as Go is concerned) register in the MMU.
A OS-dependent macro <code>get_tls</code> is defined for the assembler if the source includes
an architecture-dependent header file, like this:
@@ -356,14 +356,15 @@ an architecture-dependent header file, like this:
<p>
Within the runtime, the <code>get_tls</code> macro loads its argument register
-with a pointer to a pair of words representing the <code>g</code> and <code>m</code> pointers.
+with a pointer to the <code>g</code> pointer, and the <code>g</code> struct
+contains the <code>m</code> pointer.
The sequence to load <code>g</code> and <code>m</code> using <code>CX</code> looks like this:
</p>
<pre>
get_tls(CX)
-MOVL g(CX), AX // Move g into AX.
-MOVL m(CX), BX // Move m into BX.
+MOVL g(CX), AX // Move g into AX.
+MOVL g_m(AX), BX // Move g->m into BX.
</pre>
<h3 id="amd64">64-bit Intel 386 (a.k.a. amd64)</h3>
@@ -376,22 +377,21 @@ pointers is the same as on the 386, except it uses <code>MOVQ</code> rather than
<pre>
get_tls(CX)
-MOVQ g(CX), AX // Move g into AX.
-MOVQ m(CX), BX // Move m into BX.
+MOVQ g(CX), AX // Move g into AX.
+MOVQ g_m(AX), BX // Move g->m into BX.
</pre>
<h3 id="arm">ARM</h3>
<p>
-The registers <code>R9</code>, <code>R10</code>, and <code>R11</code>
+The registers <code>R10</code> and <code>R11</code>
are reserved by the compiler and linker.
</p>
<p>
-<code>R9</code> and <code>R10</code> point to the <code>m</code> (machine) and <code>g</code>
-(goroutine) structures, respectively.
-Within assembler source code, these pointers must be referred to as <code>m</code> and <code>g</code>;
-the names <code>R9</code> and <code>R10</code> are not recognized.\n+<code>R10</code> points to the <code>g</code> (goroutine) structure.
+Within assembler source code, this pointer must be referred to as <code>g</code>;
+the name <code>R10</code> is not recognized.
</p>
<p>
```
2. **`src/cmd/dist/buildruntime.c` のTLSマクロ定義の変更**:
`m(r)`を定義する行が削除され、`g(r)`のみが残ります。
```diff
--- a/src/cmd/dist/buildruntime.c
+++ b/src/cmd/dist/buildruntime.c
@@ -130,17 +130,14 @@ static struct {
{"386", "",
"#define get_tls(r) MOVL TLS, r\n"
"#define g(r) 0(r)(TLS*1)\n"
-"#define m(r) 4(r)(TLS*1)\n"
},
{"amd64p32", "",
"#define get_tls(r) MOVL TLS, r\n"
"#define g(r) 0(r)(TLS*1)\n"
-"#define m(r) 4(r)(TLS*1)\n"
},
{"amd64", "",
"#define get_tls(r) MOVQ TLS, r\n"
"#define g(r) 0(r)(TLS*1)\n"
-"#define m(r) 8(r)(TLS*1)\n"
},
{"arm", "",
```
3. **`src/pkg/runtime/asm_386.s` のアセンブリコード変更**:
`m`レジスタを直接参照する箇所が`g`を介したアクセスに変わります。
```diff
--- a/src/pkg/runtime/asm_386.s
+++ b/src/pkg/runtime/asm_386.s
@@ -178,7 +179,8 @@ TEXT runtime·mcall(SB), NOSPLIT, $0-4
MOVL AX, (g_sched+gobuf_g)(AX)
// switch to m->g0 & its stack, call fn
- MOVL m(CX), BX
+ MOVL g(CX), BX
+ MOVL g_m(BX), BX
MOVL m_g0(BX), SI
CMPL SI, AX // if g == m->g0 call badmcall
JNE 3(PC)
```
4. **`src/pkg/runtime/asm_arm.s` のアセンブリコード変更**:
ARM固有のレジスタ割り当てと、`save_gm`/`load_gm`関数の変更。
```diff
--- a/src/pkg/runtime/asm_arm.s
+++ b/src/pkg/runtime/asm_arm.s
@@ -19,13 +19,15 @@ TEXT _rt0_go(SB),NOSPLIT,$-4
MOVW R0, 60(R13) // save argc, argv away
MOVW R1, 64(R13)
- // set up m and g registers
- // g is R10, m is R9
+ // set up g register
+ // g is R10
MOVW $runtime·g0(SB), g
- MOVW $runtime·m0(SB), m
+ MOVW $runtime·m0(SB), R8
// save m->g0 = g0
- MOVW g, m_g0(m)
+ MOVW g, m_g0(R8)
+ // save g->m = m0
+ MOVW R8, g_m(g)
// create istack out of the OS stack
MOVW $(-8192+104)(R13), R0
@@ -38,9 +40,9 @@ TEXT _rt0_go(SB),NOSPLIT,$-4
MOVW _cgo_init(SB), R4
CMP $0, R4
B.EQ nocgo
- BL runtime·save_gm(SB);
+ BL runtime·save_g(SB);
MOVW g, R0 // first argument of _cgo_init is g
- MOVW $setmg_gcc<>(SB), R1 // second argument is address of save_gm
+ MOVW $setg_gcc<>(SB), R1 // second argument is address of save_g
BL (R4) // will clobber R0-R3
nocgo:
...
-TEXT runtime·save_gm(SB),NOSPLIT,$0
+TEXT runtime·save_g(SB),NOSPLIT,$0
MRC 15, 0, R0, C13, C0, 3 // fetch TLS base pointer
- // $runtime.tlsgm(SB) is a special linker symbol.
+ // $runtime.tlsg(SB) is a special linker symbol.
// It is the offset from the TLS base pointer to our
- // thread-local storage for g and m.
- MOVW $runtime·tlsgm(SB), R11
+ // thread-local storage for g.
+ MOVW $runtime·tlsg(SB), R11
ADD R11, R0
MOVW g, 0(R0)
- MOVW m, 4(R0)
RET
-// load_gm loads the g and m registers from pthread-provided
+// load_g loads the g register from pthread-provided
// thread-local memory, for use after calling externally compiled
// ARM code that overwrote those registers.
-TEXT runtime·load_gm(SB),NOSPLIT,$0
+TEXT runtime·load_g(SB),NOSPLIT,$0
MRC 15, 0, R0, C13, C0, 3 // fetch TLS base pointer
- // $runtime.tlsgm(SB) is a special linker symbol.
+ // $runtime.tlsg(SB) is a special linker symbol.
// It is the offset from the TLS base pointer to our
- // thread-local storage for g and m.
- MOVW $runtime·tlsgm(SB), R11
+ // thread-local storage for g.
+ MOVW $runtime·tlsg(SB), R11
ADD R11, R0
MOVW 0(R0), g
- MOVW 4(R0), m
RET
-// void setmg_gcc(M*, G*); set m and g called from gcc.
-TEXT setmg_gcc<>(SB),NOSPLIT,$0
- MOVW R0, m
- MOVW R1, g
- B runtime·save_gm(SB)
+// void setg_gcc(M*, G*); set m and g called from gcc.
+TEXT setg_gcc<>(SB),NOSPLIT,$0
+ MOVW R0, g
+ B runtime·save_g(SB)
```
## コアとなるコードの解説
上記の変更箇所は、`m`ポインタの扱いがどのように変わったかを明確に示しています。
1. **`doc/asm.html` の更新**:
これはGoアセンブリ言語の公式ドキュメントの一部です。以前は、Goランタイムが`m`と`g`の両方を特別なレジスタ(ARMの場合)またはTLSを介して管理していると説明されていました。この変更により、`m`に関する記述が削除され、`g`のみが直接管理される対象として残ります。また、`g`と`m`をロードするアセンブリの例も更新され、`g`をロードした後、`g`構造体のメンバとして`m`をロードする新しいパターン(`MOVL g_m(AX), BX`)が示されています。これは、`m`が`g`の内部にネストされたフィールドになったことを反映しています。
2. **`src/cmd/dist/buildruntime.c` のTLSマクロ定義の変更**:
このファイルは、Goのビルドシステムがランタイムのアセンブリコードで使用するTLS関連のマクロを定義しています。以前は、`g`と`m`の両方に対応するTLSオフセット(`g(r)`と`m(r)`)が定義されていました。この変更では、`m(r)`の定義が完全に削除されています。これは、`m`がTLSから直接アクセスされなくなったことを意味し、`g->m`という新しいアクセスパスが採用されたことと一致します。
3. **`src/pkg/runtime/asm_386.s` のアセンブリコード変更**:
`runtime·mcall`関数は、Goランタイムがゴルーチンを切り替える際などに使用される重要なアセンブリ関数です。以前は、現在のM(OSスレッド)へのポインタを`m(CX)`というマクロを使って直接ロードしていました。変更後は、まず`g`へのポインタを`g(CX)`でロードし、その`g`ポインタから`g_m(BX)`というオフセットを使って`m`へのポインタをロードしています。これは、`m`が`g`構造体のフィールドとしてアクセスされるようになったことを示す典型的な例です。
4. **`src/pkg/runtime/asm_arm.s` のアセンブリコード変更**:
ARMアーキテクチャの初期化コード(`_rt0_go`)では、以前は`g`をR10に、`m`をR9に直接割り当てていました。変更後は、`m`をR8にロードし、そのR8を`g`構造体の`g_m`フィールドに保存しています。これにより、R9レジスタが解放されます。
また、Cgo呼び出し時にレジスタを保存・復元するための関数も変更されています。以前の`runtime·save_gm`と`runtime·load_gm`は`g`と`m`の両方をTLSに保存・復元していましたが、新しい`runtime·save_g`と`runtime·load_g`は`g`のみを扱います。これは、`m`が`g`を介してアクセスされるため、`m`自体をTLSに直接保存する必要がなくなったためです。`setmg_gcc`も`setg_gcc`に名前が変更され、`g`のみを引数として受け取るようになっています。
これらの変更は、Goランタイムの内部で`m`へのアクセスがどのように抽象化され、`g`構造体を通じて一元化されたかを示しています。これにより、コードの複雑性が軽減され、特定のアーキテクチャにおけるレジスタの制約が解消されました。
## 関連リンク
* Go CL 109050043: [https://golang.org/cl/109050043](https://golang.org/cl/109050043)
## 参考にした情報源リンク
* Go言語の公式ドキュメント (doc/asm.html)
* Goランタイムのソースコード (src/pkg/runtime/asm_*.s, src/cmd/dist/buildruntime.c など)
* GoのM-G-Pモデルに関する一般的な情報 (Goスケジューラ、ゴルーチン、OSスレッドの関係)
* ARMアーキテクチャのレジスタに関する情報
* Google Native Client (NaCl) の制約に関する情報
* スレッドローカルストレージ (TLS) に関する一般的な情報
* GoのCgoに関する情報
* Goのベンチマークに関する情報