怎么避免内存逃逸?
<h2><span style="font-weight:bold;"/><span style="font-weight:bold;">怎么答</span><span style="font-weight:bold;"/><span style="font-weight:bold;"> </span></h2>在<code>runtime/stubs.go:133</code>有个函数叫<code>noescape</code>。<code>noescape</code>可以在逃逸分析中隐藏一个指针。让这个指针在逃逸分析中不会被检测为逃逸。
<pre class="has"><code> // 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)
}
</code></pre> <h2><span style="font-weight:bold;"/><span style="font-weight:bold;">举例</span><span style="font-weight:bold;"/><span style="font-weight:bold;"> </span></h2> <ul><li>通过一个例子加深理解,接下来尝试下怎么通过 <code>go build -gcflags=-m</code> 查看逃逸的情况。</li></ul><pre class="has"><code>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
}
</code></pre>
执行<code>go build -gcflags=-m main.go</code>
<pre class="has"><code>$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
</code></pre>
其中主要看中间一小段
<pre class="has"><code>./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
</code></pre> <h2><span style="font-weight:bold;"/><span style="font-weight:bold;">解释</span><span style="font-weight:bold;"/><span style="font-weight:bold;"> </span></h2> <ul><li>
上段代码对<code>A</code>和<code>ATrick</code>同样的功能有两种实现:他们包含一个 <code>string</code> ,然后用 <code>String()</code> 方法返回这个字符串。但是从逃逸分析看<code>ATrick</code> 版本没有逃逸。
</li><li><code>noescape()</code> 函数的作用是遮蔽输入和输出的依赖关系。使编译器不认为 <code>p</code> 会通过 <code>x</code> 逃逸, 因为 <code>uintptr()</code> 产生的引用是编译器无法理解的。
</li><li>内置的 <code>uintptr</code> 类型是一个真正的指针类型,但是在编译器层面,它只是一个存储一个 <code>指针地址</code> 的 <code>int</code> 类型。代码的最后一行返回 <code>unsafe.Pointer</code> 也是一个 <code>int</code>。
</li><li><code>noescape()</code> 在 <code>runtime</code> 包中使用 <code>unsafe.Pointer</code> 的地方被大量使用。如果作者清楚被 <code>unsafe.Pointer</code> 引用的数据肯定不会被逃逸,但编译器却不知道的情况下,这是很有用的。
</li><li>面试中秀一秀是可以的,如果在实际项目中如果使用这种unsafe包大概率会被同事打死。不建议使用! 毕竟包的名字就叫做 <code>unsafe</code>, 而且源码中的注释也写明了 <code>USE CAREFULLY!</code>。
</li></ul><h4><span style="font-weight:bold;"/><span style="font-weight:bold;">文章推荐:</span><span style="font-weight:bold;"/></h4> <ul><li>golang面试题:简单聊聊内存逃逸?</li><li>golang面试题:字符串转成byte数组,会发生内存拷贝吗?</li><li>golang面试题:翻转含有<code>中文、数字、英文字母</code>的字符串</li><li>golang面试题:拷贝大切片一定比小切片代价大吗?</li><li>golang面试题:能说说uintptr和unsafe.Pointer的区别吗?</li></ul><h6><span style="font-weight:bold;"/><span style="font-weight:bold;">如果你想每天学习一个知识点?</span><span style="font-weight:bold;"/></h6> <figure style="text-align:center;">