マネージド言語(Managed language)とは何か。
マネージド言語とは何か?
マネージド言語とは、アンマネージド言語、すなわちプログラマーが作成したロジックから大きく逸脱することなく実行のみを行う言語とは異なり、GC、ランタイム最適化、グリーンスレッド、同時実行処理などをランタイムで実行することで、ユーザーが危険な低レベル管理を行う必要をなくしてくれる言語である。このような言語の場合、ビジネスロジックのみに集中して開発に没頭できるという利点があるが、その反面、プログラマーの直感と実際のプログラムの動作が異なる場合があり、精巧なランタイムチューニングが必要になることもある。まず、マネージド言語の中で最もミニマリスト哲学に忠実であり、アセンブリが誠実なGo言語について見ていくことにする。
Go言語のバイナリ構造
| .text | .data | .gopclntab, .typelink など |
|---|---|---|
| 実行される機械語コード | 保存されるデータ | 言語ランタイムセクション |
| Go言語はユーザーが入力した通りに1:1で機械語翻訳を行うわけではないため、.textセクションのロジックは言語ランタイムセクションとも密接に関連している。 | ||
| また、ユーザーが別途記述していないruntime.printnl()のような関数が.textセクションのアセンブリに追加される。 | ||
| このような自動的なコード挿入を通じて、Go言語は開発者が手動管理から解放されるよう支援している。 |
Goにおけるmain関数部分の確認
まず、簡単なサンプルソースmain.goを作成し、mainからAMD64マシンにおいて見ていくことにする。
1package main
2
3func sayHello(msg string) {
4 println(msg)
5}
6
7func main() {
8 sayHello("Hello World")
9}
10
その後、以下のようにビルドする。
1go build main.go
2
Goは容易な低レベルデバッグのためにgo toolをサポートしている。 go toolにおいてメインパッケージからメイン関数分のアセンブリのみを確認するために、この構文を入力する。
1go tool objdump -s "main\.main" ./main
2
アセンブリ
1TEXT main.main(SB) /home/yjlee/compare-assembly/go/main.go
2 main.go:7 0x468f60 493b6610 CMPQ SP, 0x10(R14)
3 main.go:7 0x468f64 762f JBE 0x468f95
4 main.go:7 0x468f66 55 PUSHQ BP
5 main.go:7 0x468f67 4889e5 MOVQ SP, BP
6 main.go:7 0x468f6a 4883ec10 SUBQ $0x10, SP
7 main.go:8 0x468f6e 90 NOPL
8 main.go:4 0x468f6f e8cca3fcff CALL runtime.printlock(SB)
9 main.go:4 0x468f74 488d05da290100 LEAQ 0x129da(IP), AX
10 main.go:4 0x468f7b bb0b000000 MOVL $0xb, BX
11 main.go:4 0x468f80 e83bacfcff CALL runtime.printstring(SB)
12 main.go:4 0x468f85 e8f6a5fcff CALL runtime.printnl(SB)
13 main.go:4 0x468f8a e811a4fcff CALL runtime.printunlock(SB)
14 main.go:9 0x468f8f 4883c410 ADDQ $0x10, SP
15 main.go:9 0x468f93 5d POPQ BP
16 main.go:9 0x468f94 c3 RET
17 main.go:7 0x468f95 e8e6afffff CALL runtime.morestack_noctxt.abi0(SB)
18 main.go:7 0x468f9a ebc4 JMP main.main(SB)
19
- 現在のゴルーチンスタックフレーム空間が十分であるか、ゴルーチン制御ブロックレジスタ(R14)内部のスタックガード値と現在のスタックポインタ(SP)をCMPQで比較した後、不足している場合はスタック拡張のためのEntrypointである0x468f95アドレスへジャンプ(JBE)する。
- 以前のベースポインタを保存するためにPUSHQ BPでスタックに挿入する。
- ベースポインタ(BP)レジスタに現在のスタックポインタ(SP)をコピーし、関数開始時のスタック基準点を固定する。
- その後、16バイト分のローカル変数スタック空間を割り当て(SUBQ $0x10, SP)、NOPLを用いて仮想命令を埋め、CPUキャッシュの整列を行う。
- Goランタイムにおいて内部文字列標準出力の同期のためにruntime.printlock(SB)を呼び出してロックをかける。
- LEAQ命令を利用し、定数として割り当てられた文字列("Hello World")の開始アドレスを、汎用レジスタのうちGo ABI規格に従って第一引数として使用されるAXに格納する。
- その後、文字列の長さを表す値を第二引数レジスタであるBXに格納する。(MOVL $0xb, BX、すなわち10進数で11)
- runtime.printstring(SB)を呼び出し、渡されたAX(データアドレス)とBX(長さ)情報に基づいてコンソールに出力する。
- 改行処理のためにruntime.printnl(SB)を呼び出す。
- 出力が完了したため、runtime.printunlock(SB)を通じてロックを解除する。
- ADDQ $0x10, SPにより割り当てていた16バイトのスタックメモリを復旧する。
- POPQ BPにより以前のベースポインタを復元する。
- RETを通じて関数を呼び出した地点へ制御権を戻す。
- もし最初のスタック検査で空間が不足していた場合、0x468f95アドレスのruntime.morestack_noctxt.abi0(SB)を呼び出し、マネージド言語らしくスタックランタイムを動的に拡張する。
- スタック拡張が完了すると、再びmain.main(SB)の入口へ復帰(JMP)する。ご覧の通り、ビジネスロジックのアセンブリは非常に明確であり、軽量なランタイム管理のみが付加された形態である。
最適化がない場合
上記の形態は、Goコンパイラにおいて別途離れている2つの関数を自動的にインライン化して最適化した結果である。しかし、学習のために今回はsayHelloをインライン化しないようにする。これを行うために、以下のフラグでソースをコンパイルする。
1go build -gcflags="-l" main.go
2
シェルで結果を確認すると、重複するアセンブリが発見される。
1yjlee@elegant:~/compare-assembly/go$ go build -gcflags="-l" main.go
2
3go tool objdump -s "main\.sayHello" ./main
4TEXT main.sayHello(SB) /home/yjlee/compare-assembly/go/main.go
5 main.go:3 0x468f60 493b6610 CMPQ SP, 0x10(R14)
6 main.go:3 0x468f64 7636 JBE 0x468f9c
7 main.go:3 0x468f66 55 PUSHQ BP
8 main.go:3 0x468f67 4889e5 MOVQ SP, BP
9 main.go:3 0x468f6a 4883ec10 SUBQ $0x10, SP
10 main.go:5 0x468f6e 4889442420 MOVQ AX, 0x20(SP)
11 main.go:5 0x468f73 48895c2428 MOVQ BX, 0x28(SP)
12 main.go:4 0x468f78 e8c3a3fcff CALL runtime.printlock(SB)
13 main.go:4 0x468f7d 488b442420 MOVQ 0x20(SP), AX
14 main.go:4 0x468f82 488b5c2428 MOVQ 0x28(SP), BX
15 main.go:4 0x468f87 e834acfcff CALL runtime.printstring(SB)
16 main.go:4 0x468f8c e8efa5fcff CALL runtime.printnl(SB)
17 main.go:4 0x468f91 e80aa4fcff CALL runtime.printunlock(SB)
18 main.go:5 0x468f96 4883c410 ADDQ $0x10, SP
19 main.go:5 0x468f9a 5d POPQ BP
20 main.go:5 0x468f9b c3 RET
21 main.go:3 0x468f9c 4889442408 MOVQ AX, 0x8(SP)
22 main.go:3 0x468fa1 48895c2410 MOVQ BX, 0x10(SP)
23 main.go:3 0x468fa6 e8d5afffff CALL runtime.morestack_noctxt.abi0(SB)
24 main.go:3 0x468fab 488b442408 MOVQ 0x8(SP), AX
25 main.go:3 0x468fb0 488b5c2410 MOVQ 0x10(SP), BX
26 main.go:3 0x468fb5 eba9 JMP main.sayHello(SB)
27
インライン化を解除すると、関数呼び出し規格に合わせてパラメータ(AX, BX)を保存するために、スタックポインタオフセットである0x20(SP)等に値を再びロードするMOVQ演算が挿入される。すなわち、コンパイラが最適化の対象とするのは、このような不必要なメモリ移動演算および呼び出しオーバーヘッドであることが確認された。
次回
次回はGo言語におけるif文、switch文を扱う予定である。今後時間があれば、Goランタイムセクションについても分析する予定である。