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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,117评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,328评论 1 293
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,839评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,007评论 0 206
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,384评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,629评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,880评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,593评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,313评论 1 243
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,575评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,066评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,392评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,052评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,082评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,844评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,662评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,575评论 2 270

推荐阅读更多精彩内容