マネージド言語とは何か?
マネージド言語とは何か?
マネージド言語とは、アンマネージド言語、すなわちプログラマーが記述したロジックから大きく逸脱することなく実行のみを行う言語とは異なり、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}
その後、以下のようにビルドする。
1go build main.go
Goは容易な低レベルデバッグのために go tool をサポートしている。go tool において、メインパッケージ内のメイン関数のみのアセンブリを確認するために、以下の構文を入力する。
1go tool objdump -s "main\.main" ./main
アセンブリ
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)
- 現在のスレッドに進入したかをCMPQで比較した後、条件を満たせばEntrypoint 0x468f95へジャンプする。
- 進入点を
PUSHQ BPでスタックに挿入する。 - 最も最近にデータが積載されたレジスタSPに、関数開始時のスタック開始地点を指定して、ローカル変数参照時の進入点を固定する。
- その後16バイト分のローカル変数スタックを予約し (
SUBQ $0x10, SP)、NOPLを用いて数バイトを埋めることでCPUキャッシュのアライメントを行う。 - Go Runtimeにおいて文字列バッファの出力ロックを
runtime.printlock(SB)を呼び出してかける。 - LEAQ命令を使用して、割り当てられた文字列の開始アドレスを汎用レジスタのうちデータ保存に使用するアキュムレータアドレスのAXに保存する。
- その後、演算補助および一時データ保存に使用するBXレジスタに文字列長11を保存する (
MOVL $0xb, BX)。 - runtime.printstring(SB)によりSB側へアキュムレータ情報を出力する。
- 一行の空行もruntime.printnl(SB)によりSB側へ書き込む。
- 文字列バッファをruntime.printunlock(SB)で解放する。
- ADDQ $0x10, SPで借りた16バイトのスタックメモリを返還する。 - 最初に進入点をスタックに入れて通知したため、ここでPOPQ BPによりスタックから進入点を取り除いてから返還シグナルを出す。
- その後、runtime.morestack_noctxt.abi0(SB)により、マネージド言語らしく十分なスタックを割り当て、GCなどのランタイムをセットアップする。
- 管理されたmain.main(SB)アドレスへ移動する。
ご覧の通り、ビジネスロジックのアセンブリは非常に明確であり、軽量なランタイム管理のみが付加された形態となっている。
最適化が行われない場合
上記の形態は、Goコンパイラが本来分離されている2つの関数を自動的にインライン化して最適化した結果である。しかし、学習のために今回は sayHello をインライン化しないようにする。
これを行うために、次のフラグを使用してソースをコンパイルする。
1 go build -gcflags="-l" main.go
シェルで結果を出力すると、重複するアセンブリが発見される。
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)
27yjlee@elegant:~/compare-assembly/go$ go tool objdump -s "main\.sayHello" ./main
28TEXT main.sayHello(SB) /home/yjlee/compare-assembly/go/main.go
29 main.go:3 0x468f60 493b6610 CMPQ SP, 0x10(R14)
30 main.go:3 0x468f64 7636 JBE 0x468f9c
31 main.go:3 0x468f66 55 PUSHQ BP
32 main.go:3 0x468f67 4889e5 MOVQ SP, BP
33 main.go:3 0x468f6a 4883ec10 SUBQ $0x10, SP
34 main.go:5 0x468f6e 4889442420 MOVQ AX, 0x20(SP)
35 main.go:5 0x468f73 48895c2428 MOVQ BX, 0x28(SP)
36 main.go:4 0x468f78 e8c3a3fcff CALL runtime.printlock(SB)
37 main.go:4 0x468f7d 488b442420 MOVQ 0x20(SP), AX
38 main.go:4 0x468f82 488b5c2428 MOVQ 0x28(SP), BX
39OVQ 0x20(SP), AX
40 main.go:4 0x468f82 488b5c2428 MOVQ 0x28(SP), BX
41 main.go:4 0x468f87 e834acfcff CALL runtime.printstring(SB)
42 main.go:4 0x468f8c e8efa5fcff CALL runtime.printnl(SB)
43 main.go:4 0x468f91 e80aa4fcff CALL runtime.printunlock(SB)
44 main.go:5 0x468f96 4883c410 ADDQ $0x10, SP
45 main.go:5 0x468f9a 5d POPQ BP
46 main.go:5 0x468f9b c3 RET
47 main.go:3 0x468f9c 4889442408 MOVQ AX, 0x8(SP)
48 main.go:3 0x468fa1 48895c2410 MOVQ BX, 0x10(SP)
49 main.go:3 0x468fa6 e8d5afffff CALL runtime.morestack_noctxt.abi0(SB)
50 main.go:3 0x468fab 488b442408 MOVQ 0x8(SP), AX
51 main.go:3 0x468fb0 488b5c2410 MOVQ 0x10(SP), BX
52 main.go:3 0x468fb5 eba9 JMP main.sayHello(SB)
53yjlee@elegant:~/compare-assembly/go$
すなわち、コンパイラが最適化しているのは、このような重複演算や非効率的なループアンローリングなどが対象であることが確認された。
次回について
次回はGo言語におけるif文、switch文について扱う予定である。今後時間が取れれば、Goランタイムセクションについても分析する予定である。