在很多golang代码应该你看过类似这样的代码:
//go:noescape
func Cas(ptr *uint32, old, new uint32) bool
有同学会问了,//
这不是注释吗?确实,golang的编译器指令是以注释的形式存在的。
那如//go:
就是Go语言编译指示的实现方式。
编译器指令
(1)//line filename:line
//line filename:line
是一个特殊的注释,用于指定源代码中的行号和文件名,它通常用于生成代码或者调试时,帮助开发者更好地定位问题。
例如,如果你在调试一个使用了代码生成器的程序时遇到了问题,你可以使用//line
注释来查看生成的代码。
样例:
package main
//line xxxx.go:10
var h1 notInHeap1
func main() {
}
go run .
编译输出:
xxxx.go:10: undefined: notInHeap1
//line
注释只是一个注释,不会影响代码的执行,它只是用于指定行号和文件名,以便在编译错误或调试时更好地定位问题。
(2)//go:noinline
//go:noinline
作用是禁止当前函数内联。inline
是编译器优化代码的一种手段,将函数调用的地方替换为函数调用实际代码。
优势如下:
-
减少函数调用的开销,提升性能,如果要进入函数需要存储函数入栈和出栈; -
消除分支,充分利用CPU的cache的空间局部性和指令的顺序性;
同时也带来一些问题,如下:
-
同一个函数导致在多个调用替换,增加code size; -
如果代码过多,导致一个函数膨胀或者由于分支过多,可能降低缓存的命中率;
样例:
package main
//go:noinline
func add(a, b int) int {
var c = 1000
return a + b + c
}
func main() {
_ = add(1, 2)
}
go tool compile -S main.go
编译输出:
MOVD $1, R0
MOVD $2, R1
PCDATA $1, $0
CALL <unlinkable>.add(SB) // 如果没有加go:noinline,默认不会直接调用add函数,而是直接嵌入
LDP -8(RSP), (R29, R30)
ADD $32, RSP
RET (R30)
(3)//go:noescape
//go:noescape
作用是禁止变量逃逸。
逃逸是什么?编译器将自动地将超出自身生命周期的变量,从函数栈转移到堆中,逃逸就是指这种行为。 golang
就是自动将函数外部引用的变量转义到堆栈中,从而保障其他地方使用不会为空或者异常。
优势如下:
-
如果变量不逃逸,GC压力就会很小,提升性能;
同时也带来一些问题,如下:
-
如果函数外部使用已经销毁的变量,就会异常或者core;
样例:
// 执行文件:main.go
import (
"fmt"
)
//go:noescape
func noescape(d []byte) (b []byte)
func escape(d []byte) (b []byte)
// 汇编文件:main_amd64.s
// +build amd64
#include "textflag.h"
// func noescape(d []byte) (b []byte)
TEXT ·noescape(SB),NOSPLIT,$0-48
MOVQ d_base+0(FP), AX
MOVQ AX, b_base+24(FP)
MOVQ d_len+8(FP), AX
MOVQ AX, b_len+32(FP)
MOVQ d_cap+16(FP),AX
MOVQ AX, b_cap+40(FP)
RET
// func escape(d []byte) (b []byte)
TEXT ·escape(SB),NOSPLIT,$0-48
MOVQ d_base+0(FP), AX
MOVQ AX, b_base+24(FP)
MOVQ d_len+8(FP), AX
MOVQ AX, b_len+32(FP)
MOVQ d_cap+16(FP),AX
MOVQ AX, b_cap+40(FP)
RET
// 测试文件:main_test.go
package main
import (
"testing"
)
func BenchmarkNoescape(b *testing.B) {
buf := "hello world"
for n := 0; n < b.N; n++ {
noescape([]byte(buf))
}
}
func BenchmarkEscape(b *testing.B) {
buf := "hello world"
for n := 0; n < b.N; n++ {
escape([]byte(buf))
}
}
noescape
加上go:noescape
,escape
什么都不加,执行GOARCH=amd64 go test -bench .
执行输出:
BenchmarkNoescape-10 166318081 7.042 ns/op
BenchmarkEscape-10 40982848 27.66 ns/op
可以看出没有内存逃逸的noescape
的性能要好很多。
(4)//go:norace
//go:norace
作用是跳过竞态检测。
优势如下:
-
由于 Goroutine
很方便写并行逻辑,但是往往我们再代码中需要通过go run -race xxx.go
测试是否有竞态,会导致编译慢,如果加上norace
就可以跳过;
同时也带来一些问题,如下:
-
需要开发者自己控制是否变量竞争的问题;
样例:
var sum int
func sumAdd() {
sum++
}
没有加go:norace
,执行GOARCH=amd64 go run -race .
执行会提示输出:
==================
WARNING: DATA RACE
Read at 0x000001166748 by goroutine 6:
...
加了go:norace
,执行GOARCH=amd64 go run -race .
则直接不提示。
(5)//go:nosplit
//go:nosplit
作用是跳过栈溢出检测。
栈溢出是什么?Goroutine
的起始栈大小是有限制的,且比较小的,可以做到支持并发很多Goroutine
,并高效调度,如果当前栈不够用,则会自动扩展栈空间。
这样也会引入性能问题,因为需要检查栈是否需要动态扩展,如果加上//go:nosplit
,则跳过这个检查。
优势如下:
-
不执行栈溢出检查,提升性能;
同时也带来一些问题,如下:
-
可能导致stack overflow;
样例:
//go:nosplit
func split() int {
return split()
}
加上go:nosplit
,执行go run main.go
执行输出:
main.split: nosplit stack over 792 byte limit
main.split<1>
grows 16 bytes, calls main.split<1>
infinite cycle
(6)//go:linkname localname importpath.name
//go:linkname
作用是编译器使用importpath.name
作为源代码中声明为localname
的变量或函数的目标文件符号名称。
比如为了提升性能,使用底层的nanotime1
获取时间。
样例:
//go:linkname nanotime1 runtime.nanotime1
func nanotime1() int64
func main() {
fmt.Println("nanotime1: ", nanotime1())
}
加上go:linkname
,执行go run main.go
执行输出:
nanotime1: 528757773084042
注意:需要使用import _ "unsafe"
引入unsafe
包。
(7)//go:notinheap
//go:notinheap
作用是类型声明,不允许当前类型在GC堆上申请内存,主要为了提升性能。
样例:
//go:notinheap
type mcache struct {
// The following members are accessed on every malloc,
// so they are grouped here for better caching.
nextSample uintptr // trigger heap sample after allocating this many bytes
scanAlloc uintptr // bytes of scannable heap allocated
...
}
这个是mcache代码//go:notinheap
。
(8)//go:build
//go:build
是一个编译器指令,用于根据条件编译代码。语法:
//go:build condition
如果是支持linux并且是amd64,可以写//go:build linux && amd64
样例:
//go:build amd64 !wasm
func main() {
fmt.Println("nanotime1: ", nanotime1())
}
如果执行GOARCH=amd32 go run .
后输出:
go: unsupported GOOS/GOARCH pair darwin/amd32
表示不支持当前类型,需要执行GOARCH=amd64 go run .
才能编译通过。
(9)其他编译指令
-
go:systemstack
一个函数必须在系统栈上运行,这个会通过一个特殊的函数前引(prologue)动态地验证。 -
go:nowritebarrier
告知编译器如果以下函数包含了写屏障,触发一个错误。 -
go:yeswritebarrierrec
告知编译器如果以下函数以及它调用的函数(递归下去),直到一个go:yeswritebarrierrec
为止,包含了一个写屏障的话,触发一个错误。
编译命令行
用法:
go tool compile [flags] 文件...
其中flags
可以加如下指令:
-D path
设置本地导入的相对路径。
-I dir1 -I dir2
在查阅$GOROOT/pkg/$GOOS_$GOARCH后,在dir1、dir2等目录中搜索导入的包。
-L
在错误消息中显示完整的文件路径。
-N
禁用优化。
-S
将汇编列表打印到标准输出(仅限代码)。
-S -S
将汇编列表打印到标准输出(代码和数据)。
-V
打印编译器版本并退出。
-asmhdr 文件
将程序集头文件写入文件。
-asan
插入对 C/C++ 地址清理器的调用。
-buildid id
将 id 记录为导出元数据中的构建 id。
-blockprofile 文件
将编译的块配置文件写入文件。
-c int
编译期间的并发。设置 1 表示无并发(默认为 1)。
-complete
假设包没有非 Go 组件。
-cpuprofile file
将编译的CPU配置文件写入文件。
-dynlink
允许引用共享库中的 Go 符号(实验性)。
-e
删除报告错误数的限制(默认限制为 10)。
-goversion string
指定运行时所需的 go 工具版本。
当运行时 go 版本与 goversion 不匹配时退出。
-h
在检测到第一个错误时停止并显示堆栈跟踪。
-导入cfg文件
从文件中读取导入配置,在该文件中,设置 importmap、packagefile 以指定导入解析。
-installsuffix
suffix在 $GOROOT/pkg/$GOOS_$GOARCH_suffix,而不是 $GOROOT/pkg/$GOOS_$GOARCH中寻找包。
-l
禁用内联。
-lang version
设置要编译的语言版本,如-lang=go1.12,默认为当前版本。
-linkobj 文件
将特定于链接器的对象写入文件,将特定于编译器的对象写入通常的输出文件(由 -o 指定)。
没有这个标志,-o 输出是链接器和编译器输入的组合。
-m
打印优化决策。更高的值或重复产生更多的细节。
-memprofile file
将编译的内存配置文件写入文件。
-memprofilerate rate
为编译设置 runtime.MemProfileRate 以进行评分。
-msan
插入对 C/C++ 内存清理器的调用。
-mutexprofile file
将编译的互斥配置文件写入文件。
-nolocalimports
禁止本地(相对)导入。
-o file
将对象写入文件(默认 file.o 或使用 -pack,file.a)。
-p path
为正在编译的代码设置预期的包导入路径,并诊断会导致循环依赖的导入。
-pack
写一个包(存档)文件而不是目标文件
-race
在启用竞争检测器的情况下编译。
-s
警告可以简化的复合文字。
-shared
生成可以链接到共享库的代码。
-spectre list
在列表(所有、索引、ret)中启用幽灵缓解。
-traceprofile file
将执行跟踪写入文件。
-trimpath prefix
从记录的源文件路径中删除前缀。
与调试信息相关的标志:
-dwarf
生成 DWARF 符号。
-dwarflocationlists
在优化模式下将位置列表添加到 DWARF。
-gendwarfinl int
生成 DWARF 内联信息记录(默认 2)。
调试编译器本身的标志:
-E
调试符号导出。
-K
调试缺失的行号。
-d list
打印有关列表中项目的调试信息,尝试 -d help 以获得更多信息。
-live
调试活性分析。
-v
增加调试详细程度。
-%
调试非静态初始值设定项。
-W
类型检查后调试解析树。
-f
调试堆栈帧。
-i
调试行号堆栈。
-j
调试运行时初始化的变量。
-r
调试生成的包装器。
-w
调试类型检查。
参考
(1)https://pkg.go.dev/cmd/compile
原文始发于微信公众号(周末程序猿):技术总结|十分钟了解Go编译器指令
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/169410.html