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 の方が解釈が減るのでコンパイルが速い)

アセンブリもたまには読むのいいですね。※できれば読みたくない

  • このエントリーをはてなブックマークに追加

エウレカでは、一緒に働いていただける方を絶賛募集中です。募集中の職種はこちらからご確認ください!皆様のエントリーをお待ちしております!

Recommend

No UIという考え方とUIデザイナーのこれから

「どんな価値観を持った人も活躍できる会社に」 新人事制度baniera(バニエラ)を作った理由