目的
インターフェースが確定でヒープに割り当てられるというルールは特にないということを理解する。
下記の記事で、インターフェースは確定でヒープに配置されるという旨の記述があったので検証してみた。かなり古い記事なので、昔の golang はそういう仕様だったということかもしれない。
環境
$ go version
go version go1.23.2 linux/amd64
検証
./main.go:16:18: &Duck{} does not escape
より、インターフェースはスタックに配置されていることがわかる。
つまり、検証対象の記事の主張は、現在のバージョンでは間違っているということである。
package main
import "fmt"
type Sounder interface {
Sound()
}
type Duck struct{}
func (d *Duck) Sound() {
fmt.Println("quack")
}
func main() {
var d Sounder = &Duck{}
d.Sound()
}
package main
import "testing"
func Benchmark(b *testing.B) {
for i := 0; i < b.N; i++ {
main()
}
}
$ go build -gcflags "-m=2" .
# gopractice
./main.go:11:6: can inline (*Duck).Sound with cost 78 as: method(*Duck) func() { fmt.Println(... argument...) }
./main.go:15:6: can inline main with cost 67 as: func() { d := &Duck{}; d.Sound() }
./main.go:12:13: inlining call to fmt.Println
./main.go:17:9: devirtualizing d.Sound to *Duck
./main.go:17:9: inlining call to (*Duck).Sound
./main.go:17:9: inlining call to fmt.Println
./main.go:12:14: "quack" escapes to heap:
./main.go:12:14: flow: {storage for ... argument} = &{storage for "quack"}:
./main.go:12:14: from "quack" (spill) at ./main.go:12:14
./main.go:12:14: from ... argument (slice-literal-element) at ./main.go:12:13
./main.go:12:14: flow: fmt.a = &{storage for ... argument}:
./main.go:12:14: from ... argument (spill) at ./main.go:12:13
./main.go:12:14: from fmt.a := ... argument (assign-pair) at ./main.go:12:13
./main.go:12:14: flow: {heap} = *fmt.a:
./main.go:12:14: from fmt.Fprintln(os.Stdout, fmt.a...) (call parameter) at ./main.go:12:13
./main.go:11:7: d does not escape
./main.go:12:13: ... argument does not escape
./main.go:12:14: "quack" escapes to heap
./main.go:17:9: "quack" escapes to heap:
./main.go:17:9: flow: {storage for ... argument} = &{storage for "quack"}:
./main.go:17:9: from "quack" (spill) at ./main.go:17:9
./main.go:17:9: from ... argument (slice-literal-element) at ./main.go:17:9
./main.go:17:9: flow: fmt.a = &{storage for ... argument}:
./main.go:17:9: from ... argument (spill) at ./main.go:17:9
./main.go:17:9: from fmt.a := ... argument (assign-pair) at ./main.go:17:9
./main.go:17:9: flow: {heap} = *fmt.a:
./main.go:17:9: from fmt.Fprintln(os.Stdout, fmt.a...) (call parameter) at ./main.go:17:9
./main.go:16:18: &Duck{} does not escape
./main.go:17:9: ... argument does not escape
./main.go:17:9: "quack" escapes to heap
<autogenerated>:1: parameter ~p0 leaks to {heap} with derefs=0:
<autogenerated>:1: flow: {heap} = ~p0:
<autogenerated>:1: from ~p0.Sound() (call parameter) at <autogenerated>:1
$ go test -bench=. -benchmem
...
quack
...
320628 6311 ns/op 0 B/op 0 allocs/op
PASS
ok gopractice 2.073s
$ go build -o main_binary main.go
$ objdump -d main_binary > objdump.txt
000000000048f140 <main.main>:
48f140: 49 3b 66 10 cmp 0x10(%r14),%rsp
48f144: 76 47 jbe 48f18d <main.main+0x4d>
48f146: 55 push %rbp
48f147: 48 89 e5 mov %rsp,%rbp
48f14a: 48 83 ec 38 sub $0x38,%rsp
48f14e: 90 nop
48f14f: 48 8d 15 aa 95 00 00 lea 0x95aa(%rip),%rdx # 498700 <type:*+0x8700>
48f156: 48 89 54 24 28 mov %rdx,0x28(%rsp)
48f15b: 48 8d 15 b6 4f 04 00 lea 0x44fb6(%rip),%rdx # 4d4118 <runtime.buildVersion.str+0x10>
48f162: 48 89 54 24 30 mov %rdx,0x30(%rsp)
48f167: 48 8b 1d fa 32 0c 00 mov 0xc32fa(%rip),%rbx # 552468 <os.Stdout>
48f16e: 48 8d 05 e3 54 04 00 lea 0x454e3(%rip),%rax # 4d4658 <go:itab.*os.File,io.Writer>
48f175: 48 8d 4c 24 28 lea 0x28(%rsp),%rcx
48f17a: bf 01 00 00 00 mov $0x1,%edi
48f17f: 48 89 fe mov %rdi,%rsi
48f182: e8 b9 af ff ff call 48a140 <fmt.Fprintln>
48f187: 48 83 c4 38 add $0x38,%rsp
48f18b: 5d pop %rbp
48f18c: c3 ret
48f18d: e8 4e aa fd ff call 469be0 <runtime.morestack_noctxt.abi0>
48f192: eb ac jmp 48f140 <main.main>
$ go run main.go
quack
おまけ
./main.go:17:9: "quack" escapes to heap
は、fmt
パッケージのPrintln
関数に文字列を渡すと、文字列がヒープにエスケープされることを示している。
func Println(a ...any) (n int, err error)
文字列はほとんどがポインタなので、文字列を渡すために関数を呼び出しても、バイトの深いコピーは行われません。浅くコピーされた文字列は、依然として同じ基底配列を参照しています。
参考