GCに思いを馳せてunsafeなコードを書く

これは[Go Advent Calendar](https://qiita.com/advent-calendar/2019/go) 8日目の記事です．

こんにちは[taxio](https://twitter.com/taxio_tech)です．今はヒーヒー言いながら修論書いてます．
今回は`unsafe.Pointer`を使ってunsafeなことをしてみます．

## `unsafe.Pointer`とは
[GoDoc](https://golang.org/pkg/unsafe/#Pointer)より，
> Pointer represents a pointer to an arbitrary type. There are four special operations available for type Pointer that are not available for other types:
- A pointer value of any type can be converted to a Pointer.
- A Pointer can be converted to a pointer value of any type.
- A uintptr can be converted to a Pointer.
- A Pointer can be converted to a uintptr.

Goでは変数のアドレスは`&`を付けることで取得でき，ポインタ変数は型に`*`を付けることで宣言できます．
`unsafe.Pointer`は任意の型のポインタ変数として振る舞い，相互に変換が可能です．また，プリミティブ型の1つである`uintptr`との相互変換も可能で，これを使ってポインタ演算が可能になります．
如何にもunsafeな匂いがプンプンしてきますね．

しかし，GoDocに書いてある通りに気をつけて書けば，チューニングに非常に役に立つパッケージです[^1]．
今回はそんな`unsafe`パッケージを使ってパフォーマンスチューニングする......のではなく，実際にunsafeなコードを書いてみようと思います．

## How?
GoDocを読み進めていけばネタバレが書いてあるのですが，今回は一切読まずに仮説を建てて検証していきます[^bbb]．
というのも，先日Go Conference 2019 AutumnにてGoのGCについて調べて発表したのですが，これを調べている時に，「そういえば`unsafe.Pointer`というものがあったな．これをゴニョゴニョしていい感じに参照を切れば解放済みのオブジェクトを操作するunsafeなコードが書けるのでは？」という疑問が出てきました．この記事はそれがモチベーションとなって書かれています．

GoのGCアルゴリズムについては[登壇資料](https://speakerdeck.com/taxio/go-gc-algorithm-101)や[Goのソースコード](https://github.com/golang/go/blob/9341fe073e/src/runtime/mgc.go)を御覧ください．

GoはPrecise GC(正確GC)であるため[^precise]，ポインタを判定するためのセマンティクスが存在するはずです．
なので`unsafe.Pointer`が保持するアドレスを`int`に変換すれば，その変数に入っているのは最早ただの値であるため，このセマンティクスからは外れるはず...です[^netabare-uintptr]．
その状態で元々のオブジェクトやポインタを保持する他の変数の参照をRootから排除しGCを実行，`int`型に入っている**値としてのアドレス**を`unsafe.Pointer`に変換し直して元々のオブジェクトに更に変換し直した場合どうなるのでしょうか？

ちゃんとpanicしてくれるでしょうか？それとも変に動作し続けるのでしょうか？そもそもそんなことが可能なのでしょうか？
やってみた！！

## unsafeなコードを書いてみる
雑に書いてみた．

```go
type X struct {
	A int
	B string
}

func newX() *X {
	x := &X{A: 123, B: "abc"}
	fmt.Printf("before: %v\n", x)
	return x
}

func newXAsIntPtr() int {
	x := newX()
	xp := unsafe.Pointer(x)
	xup := uintptr(xp)
	return int(xup)
}

func main() {
	xip := newXAsIntPtr()
	runtime.GC()
	xp := unsafe.Pointer(uintptr(xip))
	x := (*X)(xp)
	fmt.Printf("after: %v\n", x)
}
```

結果

```sh
❯ go run main.go
before: &{123 abc}
after: &{123 abc}
```

なんでや．

### 考察
雑に書きすぎました．何が起きたか考えてみます．

そもそもGCは並行実行されている．`x := (*X)(xp)`のAllocationが間に合ってしまっている？
-> それはない．Mark setupフェーズ中はSTW状態で，Root Rescanも行われないので`runtime.GC()`が呼ばれた時点でRootからの参照ツリーに存在しないオブジェクトは死んでいるはずである．

スタック領域に確保されているのでは？
-> ありそう．確認してみよう．

```sh
❯ go build -gcflags="-m" main.go 
# command-line-arguments
./main.go:21:12: inlining call to fmt.Printf
./main.go:26:6: can inline newXAsIntPtr
./main.go:42:21: inlining call to newXAsIntPtr
./main.go:46:12: inlining call to fmt.Printf
./main.go:20:21: &X literal escapes to heap
./main.go:21:13: x escapes to heap
./main.go:21:12: newX []interface {} literal does not escape
./main.go:21:12: io.Writer(os.Stdout) escapes to heap
./main.go:46:13: x escapes to heap
./main.go:46:12: main []interface {} literal does not escape
./main.go:46:12: io.Writer(os.Stdout) escapes to heap
<autogenerated>:1: (*File).close .this does not escape
```

ちゃんとヒープ領域に確保されている．ん？待て，インライン展開されてるぞ．もしかしてこれか？

```sh
❯ go build -gcflags="-m -l" main.go
# command-line-arguments
./main.go:20:21: &X literal escapes to heap
./main.go:21:12: newX ... argument does not escape
./main.go:21:13: x escapes to heap
./main.go:46:12: main ... argument does not escape
./main.go:46:13: x escapes to heap

❯ ./main 
before: &{123 abc}
after: &{123 abc}
```

残念．

そもそもオブジェクトはGCで解放されているのか？
-> Finalizer書いてみよう．

```go
func finalizer(x *X) {
	fmt.Printf("finalize for -> %v\n", x)
	return
}

func newX() *X {
	x := &X{A: 123, B: "abc"}
	fmt.Printf("before: %v\n", x)
	runtime.SetFinalizer(x, finalizer)
	return x
}
```

```sh
❯ ./main                           
before: &{123 abc}
finalize for -> &{123 abc}
after: &{123 abc}
```

されている！！

段々分かってきました．
GoはTCMallocベースなAllocation機構を使っているので，32KB以下のサイズのオブジェクトに関しては，そのサイズごとに事前にページが作られています[^tcmalloc][^go-malloc]．つまり解放時に前後のオブジェクトを見て連結させたりする必要がなく，フィールドに対して何かしらの初期化もされていないのです(多分)．
ということは，GC後，`*X`に再変換する前に同じサイズのオブジェクトを大量に作ってしまえばどこかしらで上書きが発生するはず！

### 再挑戦
`*X`をsliceに大量にappendしてみる．

```go
func main() {
	xip := newXAsIntPtr()
	runtime.GC()

	var xs []*X
	for i := 0; i < 100000000; i++ {
		xs = append(xs, &X{A: i, B: "dummy"})
	}

	xp := unsafe.Pointer(uintptr(xip))
	x := (*X)(xp)
	fmt.Printf("after: %v\n", x)
}
```

結果

```sh
❯ ./main                           
before: &{123 abc}
finalize for -> &{123 abc}
after: &{61947 dummy}
```

ほら！！上書きされてる！！！！unsafeな挙動だ！！！！

## GoDocを読んでみる
具体的に`unsafe.Pointer`のunsafeな使い方をしてみたところで，GoDocをちゃんと読んでみます．

> A uintptr is an integer, not a reference. Converting a Pointer to a uintptr creates an integer value with no pointer semantics. Even if a uintptr holds the address of some object, the garbage collector will not update that uintptr's value if the object moves, nor will that uintptr keep the object from being reclaimed.

なるほど，わざわざ`int`に変換しなくても，`uintptr`に変換した時点でポインタのセマンティクスから外れるわけですね．これは気をつけないと踏み抜きそう．

他にも6つの`unsafe.Pointer`使用パターンとその注意が書かれていました．基本的には上記の`uintptr`の扱いに関連するものです．

> Running "go vet" can help find uses of Pointer that do not conform to these patterns, but silence from "go vet" is not a guarantee that the code is valid.

ちなみに今回のは`go vet`でちゃんとWarning出してくれます．

```sh
❯ go vet main.go 
# command-line-arguments
./main.go:42:8: possible misuse of unsafe.Pointer
```

## まとめ
- `unsafe.Pointer`から`uintptr`に変換した時点で参照のセマンティクスから外れるので，間違ってGCに回収されないように注意する必要がある
- 見かけ上動いていても，期待しているオブジェクトを指していない可能性がある
- `go vet`を無視しない．しかし完全に信用もしない．

意図せずunsafeなコードを書いてしまい，来年の[やらかしアドカレ](https://qiita.com/advent-calendar/2019/yarakashi-production)に載らないように気をつけましょう．

<details><summary>最終的なコード</summary><div>

```go
package main

import (
	"fmt"
	"runtime"
	"unsafe"
)

type X struct {
	A int
	B string
}

func finalizer(x *X) {
	fmt.Printf("finalize for -> %v\n", x)
	return
}

func newX() *X {
	x := &X{A: 123, B: "abc"}
	fmt.Printf("before: %v\n", x)
	runtime.SetFinalizer(x, finalizer)
	return x
}

func newXAsIntPtr() int {
	x := newX()
	xp := unsafe.Pointer(x)
	xup := uintptr(xp)
	return int(xup)
}

func main() {
	xip := newXAsIntPtr()
	runtime.GC()

	var xs []*X
	for i := 0; i < 100000000; i++ {
		xs = append(xs, &X{A: i, B: "dummy"})
	}

	xp := unsafe.Pointer(uintptr(xip))
	x := (*X)(xp)
	fmt.Printf("after: %v\n", x)
}
```

</div></details>

[Go Playground](https://play.golang.org/p/lWj3uJbHtrT)

## 最後に
完全に理解しているわけではないので，間違っている場合はコメントお願いします．
また，まだまだ検証できそうな派生問題がいくつもあるので，そのうちGoのソースコードとか追いながら試してみたいです[^aaa]．

ちなみに修論とGCは1㍉も関係ありません．


[^1]: 経験が無いのでUse Caseは思いつきませんが
[^precise]: https://github.com/golang/go/blob/9341fe073e/src/runtime/mgc.go#L7
[^netabare-uintptr]: **ネタバレ**: `uintptr`に変換した時点で外れます．後からGoDoc読んで知りました．
[^tcmalloc]: https://ascii.jp/elem/000/001/496/1496211/
[^go-malloc]: https://github.com/golang/go/blob/9341fe073e/src/runtime/malloc.go#L10-L15
[^aaa]: 修論終わったら
[^bbb]: 実際はちゃんと読まずに実験して，後から「書いてあるやんけー」となった感じです