問題
怎麼避免記憶體逃逸?
怎麼答
在runtime/stubs.go:133
有個函式叫noescape
。noescape
可以在逃逸分析中隱藏一個指標。讓這個指標在逃逸分析中不會被檢測為逃逸。
// noescape hides a pointer from escape analysis. noescape is
// the identity function but escape analysis doesn't think the
// output depends on the input. noescape is inlined and currently
// compiles down to zero instructions.
// USE CAREFULLY!
//go:nosplit
func noescape(p unsafe.Pointer) unsafe.Pointer {
x := uintptr(p)
return unsafe.Pointer(x ^ 0)
}
舉例
- 通過一個例子加深理解,接下來嘗試下怎麼通過
go build -gcflags=-m
檢視逃逸的情況。
package main
import (
"unsafe"
)
type A struct {
S *string
}
func (f *A) String() string {
return *f.S
}
type ATrick struct {
S unsafe.Pointer
}
func (f *ATrick) String() string {
return *(*string)(f.S)
}
func NewA(s string) A {
return A{S: &s}
}
func NewATrick(s string) ATrick {
return ATrick{S: noescape(unsafe.Pointer(&s))}
}
func noescape(p unsafe.Pointer) unsafe.Pointer {
x := uintptr(p)
return unsafe.Pointer(x ^ 0)
}
func main() {
s := "hello"
f1 := NewA(s)
f2 := NewATrick(s)
s1 := f1.String()
s2 := f2.String()
_ = s1 + s2
}
執行go build -gcflags=-m main.go
$go build -gcflags=-m main.go
# command-line-arguments
./main.go:11:6: can inline (*A).String
./main.go:19:6: can inline (*ATrick).String
./main.go:23:6: can inline NewA
./main.go:31:6: can inline noescape
./main.go:27:6: can inline NewATrick
./main.go:28:29: inlining call to noescape
./main.go:36:6: can inline main
./main.go:38:14: inlining call to NewA
./main.go:39:19: inlining call to NewATrick
./main.go:39:19: inlining call to noescape
./main.go:40:17: inlining call to (*A).String
./main.go:41:17: inlining call to (*ATrick).String
/var/folders/45/qx9lfw2s2zzgvhzg3mtzkwzc0000gn/T/go-build763863171/b001/_gomod_.go:6:6: can inline init.0
./main.go:11:7: leaking param: f to result ~r0 level=2
./main.go:19:7: leaking param: f to result ~r0 level=2
./main.go:24:16: &s escapes to heap
./main.go:23:13: moved to heap: s
./main.go:27:18: NewATrick s does not escape
./main.go:28:45: NewATrick &s does not escape
./main.go:31:15: noescape p does not escape
./main.go:38:14: main &s does not escape
./main.go:39:19: main &s does not escape
./main.go:40:10: main f1 does not escape
./main.go:41:10: main f2 does not escape
./main.go:42:9: main s1 + s2 does not escape
其中主要看中間一小段
./main.go:24:16: &s escapes to heap //這個是NewA中的,逃逸了
./main.go:23:13: moved to heap: s
./main.go:27:18: NewATrick s does not escape // NewATrick裡的s的卻沒逃逸
./main.go:28:45: NewATrick &s does not escape
解釋
-
上段程式碼對
A
和ATrick
同樣的功能有兩種實現:他們包含一個string
,然後用String()
方法返回這個字串。但是從逃逸分析看ATrick
版本沒有逃逸。 -
noescape()
函式的作用是遮蔽輸入和輸出的依賴關係。使編譯器不認為p
會通過x
逃逸, 因為uintptr()
產生的引用是編譯器無法理解的。 -
內建的
uintptr
型別是一個真正的指標型別,但是在編譯器層面,它只是一個儲存一個指標地址
的int
型別。程式碼的最後一行返回unsafe.Pointer
也是一個int
。 -
noescape()
在runtime
包中使用unsafe.Pointer
的地方被大量使用。如果作者清楚被unsafe.Pointer
引用的資料肯定不會被逃逸,但編譯器卻不知道的情況下,這是很有用的。 -
面試中秀一秀是可以的,如果在實際專案中如果使用這種unsafe包大概率會被同事打死。不建議使用! 畢竟包的名字就叫做
unsafe
, 而且原始碼中的註釋也寫明瞭USE CAREFULLY!
。
文章推薦:
- golang面試題:簡單聊聊記憶體逃逸?
- golang面試題:字串轉成byte陣列,會發生記憶體拷貝嗎?
- golang面試題:翻轉含有
中文、數字、英文字母
的字串 - golang面試題:拷貝大切片一定比小切片代價大嗎?
- golang面試題:能說說uintptr和unsafe.Pointer的區別嗎?