golang の const と var のコンパイル時解釈

golang では定数と変数をそれぞれ const と var で宣言します。とても基本的なことですが、コード内での使い方によってはコンパイラの解釈が変わるので、今回はその紹介をします。

はじめに

Golang Allstars 2 では、下記のスライドを使って登壇しました。スライドには定数のことをお話しましたが、アセンブリコードを読んだわけではなかったので、今回はアセンブリコードからのアプローチになります。

上記スライド内にある下記のコードに今回はフォーカスします。

var   Day = 24 * time.Hour
const Day = 24 * time.Hour

コードレビューをしていると、 const Day = 24 * time.Hour のように定数として宣言すればよいところを、 var Day = 24 * time.Hour として実装しているとして指摘をすることが何回かありました。

ただ、人間が「最適化できるだろう」と考えるところなので、「実際はコンパイラが処理してくれるのではないか?」と思い検証してみました。

検証コード

検証コードとして、const もしくは var で定義した値を出力するだけです。var の方は変数値の変更を行っていないのでコンパイラの方で最適化して欲しいものです。

const バージョン

package main
import (
"fmt"
"time"
)
func main() {
const dur = 7 * 24 * time.Hour // 604800000000000 (64-bit)
fmt.Println(dur)
}
var バージョン
package main
import (
"fmt"
"time"
)
func main() {
var dur = 7 * 24 * time.Hour // 604800000000000 (64-bit)
fmt.Println(dur)
}
アセンブリコード出力
go コードからアセンブリコードの出力ですが、go build の -gcflags -S オプションにて出力することができます。
go build -gcflags -S main.go
const バージョンのアセンブリコード
"".main t=1 size=173 args=0x0 locals=0x50
0x0000 00000 (/tmp/main.go:8) TEXT "".main(SB), $80-0
0x0000 00000 (/tmp/main.go:8) MOVQ (TLS), CX
0x0009 00009 (/tmp/main.go:8) CMPQ SP, 16(CX)
0x000d 00013 (/tmp/main.go:8) JLS 163
0x0013 00019 (/tmp/main.go:8) SUBQ $80, SP
0x0017 00023 (/tmp/main.go:8) MOVQ BP, 72(SP)
0x001c 00028 (/tmp/main.go:8) LEAQ 72(SP), BP
0x0021 00033 (/tmp/main.go:8) FUNCDATA $0, gclocals·69c1753bd5f81501d95132d08af04464(SB)
0x0021 00033 (/tmp/main.go:8) FUNCDATA $1, gclocals·21a8f585a14d020f181242c5256583dc(SB)
0x0021 00033 (/tmp/main.go:10) MOVQ $604800000000000, AX
0x002b 00043 (/tmp/main.go:10) MOVQ AX, "".autotmp_0+48(SP)
0x0030 00048 (/tmp/main.go:10) MOVQ $0, "".autotmp_4+56(SP)
0x0039 00057 (/tmp/main.go:10) MOVQ $0, "".autotmp_4+64(SP)
0x0042 00066 (/tmp/main.go:10) LEAQ type.time.Duration(SB), AX
0x0049 00073 (/tmp/main.go:10) MOVQ AX, (SP)
0x004d 00077 (/tmp/main.go:10) LEAQ "".autotmp_0+48(SP), AX
0x0052 00082 (/tmp/main.go:10) MOVQ AX, 8(SP)
0x0057 00087 (/tmp/main.go:10) MOVQ $0, 16(SP)
0x0060 00096 (/tmp/main.go:10) PCDATA $0, $1
0x0060 00096 (/tmp/main.go:10) CALL runtime.convT2E(SB)
0x0065 00101 (/tmp/main.go:10) MOVQ 24(SP), AX
0x006a 00106 (/tmp/main.go:10) MOVQ 32(SP), CX
0x006f 00111 (/tmp/main.go:10) MOVQ AX, "".autotmp_4+56(SP)
0x0074 00116 (/tmp/main.go:10) MOVQ CX, "".autotmp_4+64(SP)
0x0079 00121 (/tmp/main.go:10) LEAQ "".autotmp_4+56(SP), AX
0x007e 00126 (/tmp/main.go:10) MOVQ AX, (SP)
0x0082 00130 (/tmp/main.go:10) MOVQ $1, 8(SP)
0x008b 00139 (/tmp/main.go:10) MOVQ $1, 16(SP)
0x0094 00148 (/tmp/main.go:10) PCDATA $0, $1
0x0094 00148 (/tmp/main.go:10) CALL fmt.Println(SB)
0x0099 00153 (/tmp/main.go:11) MOVQ 72(SP), BP
...
var バージョンのアセンブリコード
"".main t=1 size=173 args=0x0 locals=0x50
0x0000 00000 (/tmp/main.go:8) TEXT "".main(SB), $80-0
0x0000 00000 (/tmp/main.go:8) MOVQ (TLS), CX
0x0009 00009 (/tmp/main.go:8) CMPQ SP, 16(CX)
0x000d 00013 (/tmp/main.go:8) JLS 163
0x0013 00019 (/tmp/main.go:8) SUBQ $80, SP
0x0017 00023 (/tmp/main.go:8) MOVQ BP, 72(SP)
0x001c 00028 (/tmp/main.go:8) LEAQ 72(SP), BP
0x0021 00033 (/tmp/main.go:8) FUNCDATA $0, gclocals·69c1753bd5f81501d95132d08af04464(SB)
0x0021 00033 (/tmp/main.go:8) FUNCDATA $1, gclocals·21a8f585a14d020f181242c5256583dc(SB)
0x0021 00033 (/tmp/main.go:9) MOVQ $604800000000000, AX
0x002b 00043 (/tmp/main.go:10) MOVQ AX, "".autotmp_0+48(SP)
0x0030 00048 (/tmp/main.go:10) MOVQ $0, "".autotmp_4+56(SP)
0x0039 00057 (/tmp/main.go:10) MOVQ $0, "".autotmp_4+64(SP)
0x0042 00066 (/tmp/main.go:10) LEAQ type.time.Duration(SB), AX
0x0049 00073 (/tmp/main.go:10) MOVQ AX, (SP)
0x004d 00077 (/tmp/main.go:10) LEAQ "".autotmp_0+48(SP), AX
0x0052 00082 (/tmp/main.go:10) MOVQ AX, 8(SP)
0x0057 00087 (/tmp/main.go:10) MOVQ $0, 16(SP)
0x0060 00096 (/tmp/main.go:10) PCDATA $0, $1
0x0060 00096 (/tmp/main.go:10) CALL runtime.convT2E(SB)
0x0065 00101 (/tmp/main.go:10) MOVQ 24(SP), AX
0x006a 00106 (/tmp/main.go:10) MOVQ 32(SP), CX
0x006f 00111 (/tmp/main.go:10) MOVQ AX, "".autotmp_4+56(SP)
0x0074 00116 (/tmp/main.go:10) MOVQ CX, "".autotmp_4+64(SP)
0x0079 00121 (/tmp/main.go:10) LEAQ "".autotmp_4+56(SP), AX
0x007e 00126 (/tmp/main.go:10) MOVQ AX, (SP)
0x0082 00130 (/tmp/main.go:10) MOVQ $1, 8(SP)
0x008b 00139 (/tmp/main.go:10) MOVQ $1, 16(SP)
0x0094 00148 (/tmp/main.go:10) PCDATA $0, $1
0x0094 00148 (/tmp/main.go:10) CALL fmt.Println(SB)
0x0099 00153 (/tmp/main.go:11) MOVQ 72(SP), BP
差分 - 結果
アセンブリコードを一行ごとに目grepするのは酷なので diff を用意しました。
@@ -9,7 +9,7 @@
0x001c 00028 (/tmp/main.go:8) LEAQ 72(SP), BP
0x0021 00033 (/tmp/main.go:8) FUNCDATA $0, gclocals·69c1753bd5f81501d95132d08af04464(SB)
0x0021 00033 (/tmp/main.go:8) FUNCDATA $1, gclocals·21a8f585a14d020f181242c5256583dc(SB)
- 0x0021 00033 (/tmp/main.go:9) MOVQ $604800000000000, AX
+ 0x0021 00033 (/tmp/main.go:10) MOVQ $604800000000000, AX
0x002b 00043 (/tmp/main.go:10) MOVQ AX, "".autotmp_0+48(SP)
0x0030 00048 (/tmp/main.go:10) MOVQ $0, "".autotmp_4+56(SP)
0x0039 00057 (/tmp/main.go:10) MOVQ $0, "".autotmp_4+64(SP)
この diff からわかることは、コードがコンパイラの方で解釈される行数(9行目か10行目)が変わっているだけで、メモリ内容をAXに転送しているのは変更ないです。
go の最適化オプション
go build ではオプションに何も指定しなければコードの最適化が勝手に行われます。つまり、変数で定義されているが、定数と同等の処理の場合はコンパイラの方で最適化しているようです。
さて、実際に最適化しているかどうかを明示的にコンパイラの最適化オプション外して検証してみるとしましょう。コンパイラの最適化オプションを外すためには -gcflags '-N -l' オプションをつけてビルドするだけで実行できます。
go build -gcflags '-S -N -l' main.go
const バージョン(非最適化)
"".main t=1 size=232 args=0x0 locals=0x80
0x0000 00000 (/tmp/main.go:8) TEXT "".main(SB), $128-0
0x0000 00000 (/tmp/main.go:8) MOVQ (TLS), CX
0x0009 00009 (/tmp/main.go:8) CMPQ SP, 16(CX)
0x000d 00013 (/tmp/main.go:8) JLS 222
0x0013 00019 (/tmp/main.go:8) SUBQ $128, SP
0x001a 00026 (/tmp/main.go:8) MOVQ BP, 120(SP)
0x001f 00031 (/tmp/main.go:8) LEAQ 120(SP), BP
0x0024 00036 (/tmp/main.go:8) FUNCDATA $0, gclocals·7d2d5fca80364273fb07d5820a76fef4(SB)
0x0024 00036 (/tmp/main.go:8) FUNCDATA $1, gclocals·ff5e069297bc4e135ac51ef96d4582a2(SB)
0x0024 00036 (/tmp/main.go:10) MOVQ $604800000000000, AX
0x002e 00046 (/tmp/main.go:10) MOVQ AX, "".autotmp_0+48(SP)
0x0033 00051 (/tmp/main.go:10) MOVQ $0, "".autotmp_4+80(SP)
0x003c 00060 (/tmp/main.go:10) MOVQ $0, "".autotmp_4+88(SP)
0x0045 00069 (/tmp/main.go:10) LEAQ "".autotmp_4+80(SP), AX
0x004a 00074 (/tmp/main.go:10) MOVQ AX, "".autotmp_2+56(SP)
0x004f 00079 (/tmp/main.go:10) LEAQ type.time.Duration(SB), AX
0x0056 00086 (/tmp/main.go:10) MOVQ AX, (SP)
0x005a 00090 (/tmp/main.go:10) LEAQ "".autotmp_0+48(SP), AX
0x005f 00095 (/tmp/main.go:10) MOVQ AX, 8(SP)
0x0064 00100 (/tmp/main.go:10) MOVQ $0, 16(SP)
0x006d 00109 (/tmp/main.go:10) PCDATA $0, $1
0x006d 00109 (/tmp/main.go:10) CALL runtime.convT2E(SB)
0x0072 00114 (/tmp/main.go:10) MOVQ 24(SP), AX
0x0077 00119 (/tmp/main.go:10) MOVQ 32(SP), CX
var バージョン(非最適化)
"".main t=1 size=251 args=0x0 locals=0x88
0x0000 00000 (/tmp/main.go:8) TEXT "".main(SB), $136-0
0x0000 00000 (/tmp/main.go:8) MOVQ (TLS), CX
0x0009 00009 (/tmp/main.go:8) LEAQ -8(SP), AX
0x000e 00014 (/tmp/main.go:8) CMPQ AX, 16(CX)
0x0012 00018 (/tmp/main.go:8) JLS 241
0x0018 00024 (/tmp/main.go:8) SUBQ $136, SP
0x001f 00031 (/tmp/main.go:8) MOVQ BP, 128(SP)
0x0027 00039 (/tmp/main.go:8) LEAQ 128(SP), BP
0x002f 00047 (/tmp/main.go:8) FUNCDATA $0, gclocals·7d2d5fca80364273fb07d5820a76fef4(SB)
0x002f 00047 (/tmp/main.go:8) FUNCDATA $1, gclocals·ff5e069297bc4e135ac51ef96d4582a2(SB)
0x002f 00047 (/tmp/main.go:9) MOVQ $604800000000000, AX
0x0039 00057 (/tmp/main.go:9) MOVQ AX, "".dur+48(SP)
0x003e 00062 (/tmp/main.go:10) MOVQ AX, "".autotmp_0+56(SP)
0x0043 00067 (/tmp/main.go:10) MOVQ $0, "".autotmp_4+88(SP)
0x004c 00076 (/tmp/main.go:10) MOVQ $0, "".autotmp_4+96(SP)
0x0055 00085 (/tmp/main.go:10) LEAQ "".autotmp_4+88(SP), AX
0x005a 00090 (/tmp/main.go:10) MOVQ AX, "".autotmp_2+64(SP)
0x005f 00095 (/tmp/main.go:10) LEAQ type.time.Duration(SB), AX
0x0066 00102 (/tmp/main.go:10) MOVQ AX, (SP)
0x006a 00106 (/tmp/main.go:10) LEAQ "".autotmp_0+56(SP), AX
0x006f 00111 (/tmp/main.go:10) MOVQ AX, 8(SP)
0x0074 00116 (/tmp/main.go:10) MOVQ $0, 16(SP)
0x007d 00125 (/tmp/main.go:10) PCDATA $0, $1
0x007d 00125 (/tmp/main.go:10) CALL runtime.convT2E(SB)
0x0082 00130 (/tmp/main.go:10) MOVQ 24(SP), AX
0x0087 00135 (/tmp/main.go:10) MOVQ 32(SP), CX
0x008c 00140 (/tmp/main.go:10) MOVQ AX, "".autotmp_5+72(SP)
0x0091 00145 (/tmp/main.go:10) MOVQ CX, "".autotmp_5+80(SP)
差分 - 結果
色々と差分が出ますが、注目するのは var バージョンのメモリ内容を AX に転送したあとになります。
0x002f 00047 (/tmp/main.go:9)   MOVQ    $604800000000000, AX
0x0039 00057 (/tmp/main.go:9) MOVQ AX, "".dur+48(SP)
0x003e 00062 (/tmp/main.go:10) MOVQ AX, "".autotmp_0+56(SP)
上記の部分を見ると、604800000000000という内容が SP の +48 から 8bytes 分確保(autotmp_0+56(SP) との差分)しているのがわかります。
つまり、非最適化時には無駄なアドレス確保が発生していることをこれで判断することができました。
おわりに
ここまで見ると、 const に固執しなくてもよいのかな、と思いますが、やはり気付いたときに const にしておいたほうが下記の点を考慮すると無難です。
  • 可読性(定数って一目でわかったほうがいい)
  • 効率性(var よりかも const の方が解釈が減るのでコンパイルが速い)
アセンブリもたまには読むのいいですね。※できれば読みたくない
Like what you read? Give eureka_developers a round of applause.

From a quick cheer to a standing ovation, clap to show how much you enjoyed this story.