GoでアサーションやDebug aidを行いたい。
例えば開発時はアサート失敗でpanicさせ気付ける様にしておき、リリースは完全にコードが消えていて欲しい。
そういう時は、ビルドオプションtagsを使いビルドを分岐させる事になる。
マクロ・プリプロセスがあればリリースビルドからデバッグ用コードを簡単に消せるけど、Goにはその様な機能はない。
そのためソースコード上にデバッグ用コードが残る。これが最適化で消されるかを確認した。
tl;dr
以下が判明したためGo言語でも積極的に Assertion, debugaidを利用した開発ができる。
特に定数+条件分岐を使うとリリースビルドから痕跡を完全に消せる。
ただしコミュニティにベタープラクティスとして受け入れられているかは不明。
- 空の関数は実行バイナリから消える(引数での式の評価は実行される)
- 定数 + 条件分岐の判定部分は消える
1
2
3
4
|
$ uname -a
Linux pit 3.10.0-229.el7.x86_64 #1 SMP Fri Mar 6 11:36:42 UTC 2015 x86_64 x86_64 x86_64 GNU/Linux
$ go version
go version go1.5.1 linux/amd64
|
空関数の確認
まず検証用のソースコードを準備する。
デバッグビルド
デバッグビルドを行いバイナリを確認したところcallされる(もちろん空関数ではない)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
|
$ go build -tags debug .
$ objdump -D -M intel ./build_tags | grep -A 20 '<main.main>' | lv
0000000000401000 <main.main>:
401000: 64 48 8b 0c 25 f8 ff mov rcx,QWORD PTR fs:0xfffffffffffffff8
401007: ff ff
401009: 48 8d 44 24 f8 lea rax,[rsp-0x8]
40100e: 48 3b 41 10 cmp rax,QWORD PTR [rcx+0x10]
401012: 0f 86 08 01 00 00 jbe 401120 <main.main+0x120>
401018: 48 81 ec 88 00 00 00 sub rsp,0x88
40101f: 48 c7 c0 0a 00 00 00 mov rax,0xa
401026: 48 89 44 24 40 mov QWORD PTR [rsp+0x40],rax
40102b: 48 83 f8 00 cmp rax,0x0
40102f: 0f 9f 04 24 setg BYTE PTR [rsp]
401033: e8 88 b5 06 00 call 46c5c0 <_/home/vagrant/works/playground/build_tags/debugaid.Assert>
401038: 48 8b 5c 24 40 mov rbx,QWORD PTR [rsp+0x40]
40103d: 48 89 5c 24 48 mov QWORD PTR [rsp+0x48],rbx
401042: 31 db xor ebx,ebx
401044: 48 89 5c 24 60 mov QWORD PTR [rsp+0x60],rbx
401049: 48 89 5c 24 68 mov QWORD PTR [rsp+0x68],rbx
40104e: 48 8d 5c 24 60 lea rbx,[rsp+0x60]
401053: 48 83 fb 00 cmp rbx,0x0
401057: 0f 84 bc 00 00 00 je 401119 <main.main+0x119>
40105d: 48 c7 44 24 78 01 00 mov QWORD PTR [rsp+0x78],0x1
--
401125: e9 d6 fe ff ff jmp 401000 <main.main>
40112a: 00 00 add BYTE PTR [rax],al
40112c: 00 00 add BYTE PTR [rax],al
|
リリースビルド
リリースビルドを行ってバイナリを確認したところ空関数はcall
されていない。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
|
$ go build .
$ objdump -D -M intel ./build_tags | grep -A 20 '<main.main>' | lv
0000000000401000 <main.main>:
401000: 64 48 8b 0c 25 f8 ff mov rcx,QWORD PTR fs:0xfffffffffffffff8
401007: ff ff
401009: 48 3b 61 10 cmp rsp,QWORD PTR [rcx+0x10]
40100d: 0f 86 f2 00 00 00 jbe 401105 <main.main+0x105>
401013: 48 81 ec 80 00 00 00 sub rsp,0x80
40101a: 48 c7 c0 0a 00 00 00 mov rax,0xa
401021: 48 83 f8 00 cmp rax,0x0
401025: 0f 9f c1 setg cl
401028: 48 89 44 24 40 mov QWORD PTR [rsp+0x40],rax
40102d: 31 db xor ebx,ebx
40102f: 48 89 5c 24 58 mov QWORD PTR [rsp+0x58],rbx
401034: 48 89 5c 24 60 mov QWORD PTR [rsp+0x60],rbx
401039: 48 8d 5c 24 58 lea rbx,[rsp+0x58]
40103e: 48 83 fb 00 cmp rbx,0x0
401042: 0f 84 b6 00 00 00 je 4010fe <main.main+0xfe>
401048: 48 c7 44 24 70 01 00 mov QWORD PTR [rsp+0x70],0x1
40104f: 00 00
401051: 48 c7 44 24 78 01 00 mov QWORD PTR [rsp+0x78],0x1
401058: 00 00
40105a: 48 89 5c 24 68 mov QWORD PTR [rsp+0x68],rbx
--
40110a: e9 f1 fe ff ff jmp 401000 <main.main>
...
|
ご覧の通りcall
命令が消えている。
他にも細かくいろいろと変わっている事と副作用のない比較命令が消えてくれないのが少し残念。
定数 + 条件分岐の確認
条件分岐について消えてくれる事を願って確認した。
以下の3条件でmain.main
のアセンブリを比較した。
- 分岐なし
- 定数分岐(tags
""
)
- 定数分岐(tags
"debug"
)
- 変数分岐
導入コード
分岐なし
1
2
3
|
func main() {
fmt.Println("pure")
}
|
定数分岐
1
2
3
4
5
6
7
|
func main() {
if debugaid.IsDebug {
fmt.Println("Is Debug")
} else {
fmt.Println("Isn't Debug")
}
}
|
変数分岐
定数分岐のdebugaid.IsDebug
を変数として宣言するだけ。
結果
下記の様に定数分岐では分岐なしのコードと命令数に差が出ず、また目立った分岐が追加された様子がなかった。
一方、変数分岐で導入した場合は命令数に変化が現れた。
そのため定数+分岐はコンパイル時にちゃんと消してもらえていると考えられる。
アドレス |
値 |
開始アドレス |
401000 |
終了アドレス: 分岐なし |
4010f1 |
終了アドレス: 定数分岐(tags “") |
4010f1 |
終了アドレス: 定数分岐(tags “debug”) |
4010f1 |
終了アドレス: 変数分岐 |
4011ea |
今回の結果からGo言語でもassert
,debugaid
を使った開発を行えると判断した。