これはGo Advent Calendar 8日目の記事です.
こんにちはtaxioです.今はヒーヒー言いながら修論書いてます.
今回はunsafe.Pointer
を使ってunsafeなことをしてみます.
unsafe.Pointer
とは
GoDocより,
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を読み進めていけばネタバレが書いてあるのですが,今回は一切読まずに仮説を建てて検証していきます2.
というのも,先日Go Conference 2019 AutumnにてGoのGCについて調べて発表したのですが,これを調べている時に,「そういえばunsafe.Pointer
というものがあったな.これをゴニョゴニョしていい感じに参照を切れば解放済みのオブジェクトを操作するunsafeなコードが書けるのでは?」という疑問が出てきました.この記事はそれがモチベーションとなって書かれています.
GoのGCアルゴリズムについては登壇資料やGoのソースコードを御覧ください.
GoはPrecise GC(正確GC)であるため3,ポインタを判定するためのセマンティクスが存在するはずです.
なのでunsafe.Pointer
が保持するアドレスをint
に変換すれば,その変数に入っているのは最早ただの値であるため,このセマンティクスからは外れるはず...です4.
その状態で元々のオブジェクトやポインタを保持する他の変数の参照をRootから排除しGCを実行,int
型に入っている値としてのアドレスをunsafe.Pointer
に変換し直して元々のオブジェクトに更に変換し直した場合どうなるのでしょうか?
ちゃんとpanicしてくれるでしょうか?それとも変に動作し続けるのでしょうか?そもそもそんなことが可能なのでしょうか?
やってみた!!
unsafeなコードを書いてみる
雑に書いてみた.
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)
}
結果
❯ go run main.go
before: &{123 abc}
after: &{123 abc}
なんでや.
考察
雑に書きすぎました.何が起きたか考えてみます.
そもそもGCは並行実行されている.x := (*X)(xp)
のAllocationが間に合ってしまっている?
-> それはない.Mark setupフェーズ中はSTW状態で,Root Rescanも行われないのでruntime.GC()
が呼ばれた時点でRootからの参照ツリーに存在しないオブジェクトは死んでいるはずである.
スタック領域に確保されているのでは?
-> ありそう.確認してみよう.
❯ 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
ちゃんとヒープ領域に確保されている.ん?待て,インライン展開されてるぞ.もしかしてこれか?
❯ 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書いてみよう.
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
}
❯ ./main
before: &{123 abc}
finalize for -> &{123 abc}
after: &{123 abc}
されている!!
段々分かってきました.
GoはTCMallocベースなAllocation機構を使っているので,32KB以下のサイズのオブジェクトに関しては,そのサイズごとに事前にページが作られています56.つまり解放時に前後のオブジェクトを見て連結させたりする必要がなく,フィールドに対して何かしらの初期化もされていないのです(多分).
ということは,GC後,*X
に再変換する前に同じサイズのオブジェクトを大量に作ってしまえばどこかしらで上書きが発生するはず!
再挑戦
*X
をsliceに大量にappendしてみる.
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)
}
結果
❯ ./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出してくれます.
❯ go vet main.go
# command-line-arguments
./main.go:42:8: possible misuse of unsafe.Pointer
まとめ
-
unsafe.Pointer
からuintptr
に変換した時点で参照のセマンティクスから外れるので,間違ってGCに回収されないように注意する必要がある - 見かけ上動いていても,期待しているオブジェクトを指していない可能性がある
-
go vet
を無視しない.しかし完全に信用もしない.
意図せずunsafeなコードを書いてしまい,来年のやらかしアドカレに載らないように気をつけましょう.
最終的なコード
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)
}
最後に
完全に理解しているわけではないので,間違っている場合はコメントお願いします.
また,まだまだ検証できそうな派生問題がいくつもあるので,そのうちGoのソースコードとか追いながら試してみたいです7.
ちなみに修論とGCは1㍉も関係ありません.
-
経験が無いのでUse Caseは思いつきませんが ↩
-
実際はちゃんと読まずに実験して,後から「書いてあるやんけー」となった感じです ↩
-
https://github.com/golang/go/blob/9341fe073e/src/runtime/mgc.go#L7 ↩
-
ネタバレ:
uintptr
に変換した時点で外れます.後からGoDoc読んで知りました. ↩ -
https://github.com/golang/go/blob/9341fe073e/src/runtime/malloc.go#L10-L15 ↩
-
修論終わったら ↩