Skip to content

Goでインライン展開された関数のポインタ

この関数は、関数呼び出しもなく複雑な式もないのでインライン展開される条件を満たす。

func do[T any](v T) func() T {
var f func() T
f = func() T {
return v
}
return f
}

このとき、

func main() {
f1 := do(10)
f2 := do(20)
fmt.Printf("%v 0x%x\n", f1(), **(**uintptr)(unsafe.Pointer(&f1)))
fmt.Printf("%v 0x%x\n", f2(), **(**uintptr)(unsafe.Pointer(&f2)))
// Output:
// 10 0x4829e0
// 20 0x4829c0
}

この f1f2 は異なるアドレスを持つが、どのように扱われているのか。アセンブリのコードをみると、 f1 の行は次のようになっている。

0x48207e 488d05bb0a0100 LEAQ 0x10abb(IP), AX
0x482085 e896b6f8ff CALL runtime.newobject(SB)
0x48208a 48898424e0000000 MOVQ AX, 0xe0(SP)
0x482092 488d0d47090000 LEAQ main.main.do[go.shape.int].func1(SB), CX
0x482099 488908 MOVQ CX, 0(AX)
0x48209c 48c740080a000000 MOVQ $0xa, 0x8(AX)
0x4820a4 488d0d35cf0300 LEAQ main..dict.do[int](SB), CX
0x4820ab 48894810 MOVQ CX, 0x10(AX)

途中で main.main.do[go.shape.int].func1(SB)CX に格納している。インライン展開されていても関数自体は生成されている。関数の中身は do が返している関数。

TEXT main.main.do[go.shape.int].func2(SB)
0x4829c0 488b4208 MOVQ 0x8(DX), AX
0x4829c4 c3 RET
TEXT main.main.do[go.shape.int].func1(SB)
0x4829e0 488b4208 MOVQ 0x8(DX), AX
0x4829e4 c3 RET

f1 を呼び出すときのアセンブリコードは、キャプチャした値を返しているように読める。

0x48215b 488b9424e0000000 MOVQ 0xe0(SP), DX
0x482163 488b0a MOVQ 0(DX), CX
0x482166 ffd1 CALL CX
0x482168 4889442448 MOVQ AX, 0x48(SP)

挙動をまとめると、

  • インライン展開されるので関数呼び出しごとに別の関数が作られる
  • ネストしている場合は main.main.wrap[int].do[go.shape.int].func1 のように名前が変わる
  • ネストした場合に do が返すアドレスは、wrap の中に do は1つだけなので同じ値を返す
  • ただしキャプチャされた変数の管理領域はスタック上にあるので取れる値は正しい