技术总结|十分钟了解Go编译器指令

在很多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:noescapeescape什么都不加,执行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

(0)
小半的头像小半

相关推荐

发表回复

登录后才能评论
极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!