GoSuda

マネージド言語(Managed language)とは何か。

By Lee Yunjin
views ...

マネージド言語とは何か?

マネージド言語とは、アンマネージド言語、すなわちプログラマーが作成したロジックから大きく逸脱することなく実行のみを行う言語とは異なり、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ランタイムセクションについても分析する予定である。