Golang 隐藏技能 -- 编译指令

类似C++中的 #pragma pack(2),Golang中也有一些编译指令。它们的实现方式是一些特殊的注释。

警告一下!

编译指令不是语言的一部分。它们可能是编译器实现的,编程规范中也没有对它们的描述(更正一下,现在有一部分指令的描述了https://golang.org/cmd/compile/
)。
语法:
//go:directive
编译指令的语法是一行特殊的注释,关键字//和go之间没有空格。

//go:noescape
func NewBook() (*Book) {
        b := Book{ Mice: 12, Men: 9 }
        return &b
}

这段代码在C/C++中这样做,返回的是不可用的地址,显然是要出问题的。在go中是可以的。因为逃逸分析,b将会被分配在堆上。

逃逸分析:

逃逸分析可以识别生命周期超出变量声明函数的生命周期,并将变量从栈的分配上移动到堆中 Technically we say that b escapes to the heap.

func BuildLibrary() {
        b := Book{Mice: 99: Men: 3}
        AddToCollection(&b)
}

问题: b逃逸到了堆中?
这取决于AddToCollection 对b做了什么

func AddToCollection(b *Book) {
        b.Classification = "fiction"
}

逃逸分析发现AddToCollection并没有将*book继续传递,所以此时b会被分配在栈上。

但是,如果AddToCollection做了这样的操作:

var AvailableForLoan [] *Book
func AddToCollection(b * Book){
        AvailableForLoan = append(AvailableForLoan,b)
}

AddToCollection中将bappend到了一个生命周期更长的slice中,所以b必须被分配在堆上以保证的生命周期大于AddToCollection和BuildLibrary的。逃逸分析必须知道AddToCollection对b做了什么,调用了什么func 等等,以了解值是应该分配在栈上还是堆上。这是逃逸分析的本质。

再看另一个例子:

os.File.Read
f, _ := os.Open("/tmp/foo")
buf := make([]byte, 4096)
n, _ := f.Read(buf)

我们打开一个文件,创建一个buf,然后读取数据到buf中。此时buf是在栈上还是堆上?
如上节所述,这取决于Read内部发生的事情。os.Read通过几层调用调到了syscall.Read,然后又调到了syscall.Syscall来进行操作系统调用。而syscall.Syscall是在汇编中实现的,所以Go中的编译器无法“看到”该函数的实现,因此无法判断传递的值是否为escape。由于编译器不能知道是否需要escape所以,buf只能被判定为escape。

回到//go:noescape编译指令来
假设我们要用汇编写一段glue code,类似bytes,md5,syscall 包。
我们传递的值都会被分配在堆上。即使我们知道这样做没有必要。

package bytes
//go:noescape
// IndexByte returns the index of the first instance of c in s,
// or -1 if c is not present in s.
func IndexByte(s []byte, c byte) int // ../runtime/asm_$GOARCH.s

这就是//go:noescape的意义了,这个指令告诉编译器 下面的func没有任何参数escape。编译器将会跳过对func参数的检查。
//go:escape只能用于前置声明(即 指令下的第一个func会受指令影响)
不过要格外关注的是,这个命令会使代码跳过编译器的检查。如果弄错了就会破坏内存,且没有工具能发现这一点。

//go:norace

norace指令的用法和noescape一样。Norace指令可以使编译器跳过竞争检测
鉴于竞争检测器没有已知的误报,应该没有理由将函数从其范围中排除。

//go:nosplit

我们都知道goroutine的栈是可以动态自增的。Runtime会追踪每个stack的使用情况。在运行函数之前会进行检查以确保有足够的栈空间来运行该函数。如果不够,代码先进入runtime扩充stack。
但是有时这种开销是不可接受的(偶尔也是不安全的)

//go:nosplit指令禁止stack拆分。但是这会导致一个问题,如果你的堆栈耗尽,会发生什么//go:nosplit?编译器必须确保运行函数是安全的,不能因为避免了栈检查的开销就让函数使用比被允许空间更多的内存。因为这样做的话肯定会破坏其他goroutine的内存空间。

为此,编译器维护一个名为redzone的缓冲区,一个768字节的,分配在每个goroutines的堆栈框架底部,保证可用。
编译器会跟踪每个函数的堆栈要求。当它遇到一个nosplit函数时,它会累积该函数对redzone的堆栈分配。通过这种方式,nosplit函数可以安全地对redzone缓冲区执行,同时避免在不方便的时候堆栈增长。
(关于redzone的详细描述 会单起一文)

package main
type T [256]byte // a large stack allocated type

//go:nosplit
func A(t T) {
        B(t)
}

//go:nosplit
func B(t T) {
        C(t)
}

//go:nosplit
func C(t T) {
        D(t)
}

//go:nosplit
//go:noinline
func D(t T) {}

func main() {
        var t T
        A(t)
}
# command-line-arguments
main.C: nosplit stack overflow
    744 assumed on entry to main.A (nosplit)
    480 after main.A (nosplit) uses 264
    472 on entry to main.B (nosplit)
    208 after main.B (nosplit) uses 264
    200 on entry to main.C (nosplit)
    -64 after main.C (nosplit) uses 264

上面这段程序尝试使用nosplit,但是不会编译,因为编译器检测到redzone会耗尽

我们是否应该在代码中使用//go:nosplit?可以用,但是没有必要。小函数从这种优化中获取的收益要比内联带来的收益小。上面的示例中用了//go:noinline禁止内联。否则会检测到D()什么也没做,因此编译器会优化掉整个调用树。
在所有指令中//go:nosplit是最安全的。因为它会在编译时被发现。并且不会影响程序正确性,只会影响性能。

//go:noinlie

noinlie顾名思义,告诉编译器不要inline。是否在我们的代码中应该这样做呢?我建议是不要用noinline指令的。

最后:

Go支持更多的编译指令,但不在本文讨论范围
+build是Go tool而不是编译器实现,为了过滤传递给编译器的build或test文件。
编译指令没有出现在官方文档中,编译指令使用是有风险的。如果需要使用到这些编译指令,请先翻阅相关指令在Golang源码中的使用方法。

参考文献:gos-hidden-pragmas

你可能感兴趣的:(Golang 隐藏技能 -- 编译指令)