有用 --- 它可以让我们写出更加灵活的代码。程序里常用的热路径代码的相关实例就是标准库提供的hash
包。hash
包定义了一系列常规接口并提供了几个具体实现。我们看一个例子。
package main
import (
"fmt" "hash/fnv" ) func hashIt(in string) uint64 { h := fnv.New64a() h.Write([]byte(in)) out := h.Sum64() return out } func main() { s := "hello" fmt.Printf("The FNV64a hash of '%v' is '%v'\n", s, hashIt(s)) }
构建检查逃逸分析结果:
./foo1.go:9:17: inlining call to fnv.New64a
./foo1.go:10:16: ([]byte)(in) escapes to heap ./foo1.go:9:17: hash.Hash64(&fnv.s·2) escapes to heap ./foo1.go:9:17: &fnv.s·2 escapes to heap ./foo1.go:9:17: moved to heap: fnv.s·2 ./foo1.go:8:24: hashIt in does not escape ./foo1.go:17:13: s escapes to heap ./foo1.go:17:59: hashIt(s) escapes to heap ./foo1.go:17:12: main ... argument does not escape
也就是说,hash
对象,输入字符串,以及代表输入的[]byte
全都会逃逸到堆上。我们肉眼看上去显然不会逃逸,但是接口类型限制了编译器。不通过hash
包的接口就没有办法安全地使用具体的实现。 那么效率相关的开发人员应该做些什么呢?
我们在构建Centrifuge的时候遇到了这个问题,Centrifuge在热代码路径对小字符串进行非加密hash。因此我们建立了fasthash库。构建它很直接,困难工作依旧在标准库里做。fasthash
只是在没有使用堆分配的情况下重新打包了标准库。
直接来看一下fasthash
版本的代码
package main
import (
"fmt" "github.com/segmentio/fasthash/fnv1a" ) func hashIt(in string) uint64 { out := fnv1a.HashString64(in) return out } func main() { s := "hello" fmt.Printf("The FNV64a hash of '%v' is '%v'\n", s, hashIt(s)) }
看一下逃逸分析输出
./foo2.go:9:24: hashIt in does not escape ./foo2.go:16:13: s escapes to heap ./foo2.go:16:59: hashIt(s) escapes to heap ./foo2.go:16:12: main ... argument does not escape
唯一产生的逃逸就是因为fmt.Printf()
方法的动态特性。尽管通常我们更喜欢是用标准库,但是在一些情况下需要进行权衡是否要提高分配效率。
一个小窍门
我们最后这个事情,不够实际但是很有趣。它有助我们理解编译器的逃逸分析机制。 在查看所涵盖优化的标准库时,我们遇到了一段相当奇怪的代码。
// 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) }
这个方法会让传递的指针逃过编译器的逃逸分析检查。那么这意味着什么呢?我们来设置个实验看一下。
package main
import (
"unsafe" ) type Foo struct { S *string } func (f *Foo) String() string { return *f.S } type FooTrick struct { S unsafe.Pointer } func (f *FooTrick) String() string { return *(*string)(f.S) } func NewFoo(s string) Foo { return Foo{S: &s} } func NewFooTrick(s string) FooTrick { return FooTrick{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 := NewFoo(s) f2 := NewFooTrick(s) s1 := f1.String() s2 := f2.String() }
这个代码包含两个相同任务的实现:它们包含一个字符串,并使用String()
方法返回所持有的字符串。但是,编译器的逃逸分析说明FooTrick
版本根本没有逃逸。
./foo3.go:24:16: &s escapes to heap
./foo3.go:23:23: moved to heap: s
./foo3.go:27:28: NewFooTrick s does not escape
./foo3.go:28:45: NewFooTrick &s does not escape
./foo3.go:31:33: noescape p does not escape
./foo3.go:38:14: main &s does not escape
./foo3.go:39:19: main &s does not escape
./foo3.go:40:17: main f1 does not escape
./foo3.go:41:17: main f2 does not escape
这两行是最相关的
./foo3.go:24:16: &s escapes to heap
./foo3.go:23:23: moved to heap: s
这是编译器认为NewFoo()``方法把拿了一个string类型的引用并把它存到了结构体里,导致了逃逸。但是
NewFooTrick()方法并没有这样的输出。如果去掉
noescape(),逃逸分析会把FooTrick
结构体