当前位置: 首页 > news >正文

Go 语言编译器的 //go: 详解

前言

C 语言的 #include

一上来不太好说明白 Go 语言里 //go: 是什么,我们先来看下非常简单,也是几乎每个写代码的人都知道的东西:C 语言的 #include
我猜,大部分人第一行代码都是 #include 吧。完整的就是#include <stdio.h>。意思很简单,引入一个 stdio.h。谁引入?答案是编译器。那么,# 字符的作用就是给 编译器 一个 指示,让编译器知道接下来要做什么。

编译指示

在计算机编程中,编译指示(pragma)是一种语言结构,它指示编译器应该如何处理其输入。指示不是编程语言语法的一部分,因编译器而异。

这里 Wiki 详细介绍了它,值得你看一下。

Go 语言的编译指示

官方文档 https://golang.org/cmd/compil...

形如 //go: 就是 Go 语言编译指示的实现方式。相信看过 Go SDK 的同学对此并不陌生,经常能在代码函数声明的上一行看到这样的写法。
有同学会问了,// 这不是注释吗?确实,它是以注释的形式存在的。

编译器源码 这里可以看到全部的指示,但是要注意, //go: 是连续的, //go 之间并没有空格。

常用指示详解

//go:noinline

noinline 顾名思义,不要内联。

Inline 内联

Inline,是在编译期间发生的,将函数调用调用处替换为被调用函数主体的一种编译器优化手段。Wiki: Inline 定义

使用 Inline 有一些优势,同样也有一些问题。

优势:
  • 减少函数调用的开销,提高执行速度。
  • 复制后的更大函数体为其他编译优化带来可能性,如 过程间优化
  • 消除分支,并改善空间局部性和指令顺序性,同样可以提高性能。
问题:
  • 代码复制带来的空间增长。
  • 如果有大量重复代码,反而会降低缓存命中率,尤其对 CPU 缓存是致命的。

所以,在实际使用中,对于是否使用内联,要谨慎考虑,并做好平衡,以使它发挥最大的作用。
简单来说,对于短小而且工作较少的函数,使用内联是有效益的。

内联的例子

func appendStr(word string) string {
    return "new " + word
}

执行 GOOS=linux GOARCH=386 go tool compile -S main.go > main.S
我截取有区别的部分展出它编译后的样子:

    0x0015 00021 (main.go:4)    LEAL    ""..autotmp_3+28(SP), AX
    0x0019 00025 (main.go:4)    PCDATA    $2, $0
    0x0019 00025 (main.go:4)    MOVL    AX, (SP)
    0x001c 00028 (main.go:4)    PCDATA    $2, $1
    0x001c 00028 (main.go:4)    LEAL    go.string."new "(SB), AX
    0x0022 00034 (main.go:4)    PCDATA    $2, $0
    0x0022 00034 (main.go:4)    MOVL    AX, 4(SP)
    0x0026 00038 (main.go:4)    MOVL    $4, 8(SP)
    0x002e 00046 (main.go:4)    PCDATA    $2, $1
    0x002e 00046 (main.go:4)    LEAL    go.string."hello"(SB), AX
    0x0034 00052 (main.go:4)    PCDATA    $2, $0
    0x0034 00052 (main.go:4)    MOVL    AX, 12(SP)
    0x0038 00056 (main.go:4)    MOVL    $5, 16(SP)
    0x0040 00064 (main.go:4)    CALL    runtime.concatstring2(SB)

可以看到,它并没有调用 appendStr 函数,而是直接把这个函数体的功能内联了。

那么话说回来,如果你不想被内联,怎么办呢?此时就该使用 go//:noinline 了,像下面这样写:

//go:noinline
func appendStr(word string) string {
    return "new " + word
}

编译后是:

    0x0015 00021 (main.go:4)    LEAL    go.string."hello"(SB), AX
    0x001b 00027 (main.go:4)    PCDATA    $2, $0
    0x001b 00027 (main.go:4)    MOVL    AX, (SP)
    0x001e 00030 (main.go:4)    MOVL    $5, 4(SP)
    0x0026 00038 (main.go:4)    CALL    "".appendStr(SB)

此时编译器就不会做内联,而是直接调用 appendStr 函数。

//go:nosplit

nosplit 的作用是:跳过栈溢出检测。

栈溢出是什么?

正是因为一个 Goroutine 的起始栈大小是有限制的,且比较小的,才可以做到支持并发很多 Goroutine,并高效调度。
stack.go 源码中可以看到,_StackMin 是 2048 字节,也就是 2k,它不是一成不变的,当不够用时,它会动态地增长。
那么,必然有一个检测的机制,来保证可以及时地知道栈不够用了,然后再去增长。
回到话题,nosplit 就是将这个跳过这个机制。

优劣

显然地,不执行栈溢出检查,可以提高性能,但同时也有可能发生 stack overflow 而导致编译失败。

//go:noescape

noescape 的作用是:禁止逃逸,而且它必须指示一个只有声明没有主体的函数。

逃逸是什么?

Go 相比 C、C++ 是内存更为安全的语言,主要一个点就体现在它可以自动地将超出自身生命周期的变量,从函数栈转移到堆中,逃逸就是指这种行为。

请参考我之前的文章, 逃逸分析。

优劣

最显而易见的好处是,GC 压力变小了。
因为它已经告诉编译器,下面的函数无论如何都不会逃逸,那么当函数返回时,其中的资源也会一并都被销毁。
不过,这么做代表会绕过编译器的逃逸检查,一旦进入运行时,就有可能导致严重的错误及后果。

//go:norace

norace 的作用是:跳过竞态检测
我们知道,在多线程程序中,难免会出现数据竞争,正常情况下,当编译器检测到有数据竞争,就会给出提示。如:

var sum int

func main() {
    go add()
    go add()
}

func add() {
    sum++
}

执行 go run -race main.go 利用 -race 来使编译器报告数据竞争问题。你会看到:

==================
WARNING: DATA RACE
Read at 0x00000112f470 by goroutine 6:
  main.add()
      /Users/sxs/Documents/go/src/test/main.go:15 +0x3a

Previous write at 0x00000112f470 by goroutine 5:
  main.add()
      /Users/sxs/Documents/go/src/test/main.go:15 +0x56

Goroutine 6 (running) created at:
  main.main()
      /Users/sxs/Documents/go/src/test/main.go:11 +0x5a

Goroutine 5 (finished) created at:
  main.main()
      /Users/sxs/Documents/go/src/test/main.go:10 +0x42
==================
Found 1 data race(s)

说明两个 goroutine 执行的 add() 在竞争。

优劣

使用 norace 除了减少编译时间,我想不到有其他的优点了。但缺点却很明显,那就是数据竞争会导致程序的不确定性。

总结

我认为绝大多数情况下,无需在编程时使用 //go: Go 语言的编译器指示,除非你确认你的程序的性能瓶颈在编译器上,否则你都应该先去关心其他更可能出现瓶颈的事情。

参考

  • https://dave.cheney.net/2018/...

相关文章:

  • 《2019年世界发展报告》发布,阿里巴巴助力小企业发展创造就业
  • @Service注解让spring找到你的Service bean
  • python 3.5 解决csv 读入中的'utf-8' codec can't decode办法
  • 2018 JVM 生态报告:79% 的 Java 开发者使用 Java 8
  • 微信小程序 - 使用七牛云 API 截取第 n 秒图像为封面图
  • 《netty入门与实战》笔记-03:数据传输载体 ByteBuf 介绍
  • 【转】使用 lsof 查找打开的文件
  • 实验报告五201521460014 综合渗透
  • EDMA3浅析
  • Lua与C/C++的交互
  • java网络编程之IO
  • 最好用的中间人***工具mitmproxy
  • linuxcentos忘记root管理用户密码 单用户模式维护重置密码操作指引
  • go语言学习初探(一)
  • 神经网络_1
  • 【vuex入门系列02】mutation接收单个参数和多个参数
  • 【跃迁之路】【699天】程序员高效学习方法论探索系列(实验阶段456-2019.1.19)...
  • ECMAScript6(0):ES6简明参考手册
  • Laravel5.4 Queues队列学习
  • OpenStack安装流程(juno版)- 添加网络服务(neutron)- controller节点
  • RedisSerializer之JdkSerializationRedisSerializer分析
  • Redis提升并发能力 | 从0开始构建SpringCloud微服务(2)
  • vue2.0开发聊天程序(四) 完整体验一次Vue开发(下)
  • 简单数学运算程序(不定期更新)
  • 蓝海存储开关机注意事项总结
  • 如何使用 JavaScript 解析 URL
  • 使用Gradle第一次构建Java程序
  • 使用前端开发工具包WijmoJS - 创建自定义DropDownTree控件(包含源代码)
  • 整理一些计算机基础知识!
  • #Z0458. 树的中心2
  • (8)Linux使用C语言读取proc/stat等cpu使用数据
  • (C++17) optional的使用
  • (Git) gitignore基础使用
  • (pojstep1.3.1)1017(构造法模拟)
  • (二)基于wpr_simulation 的Ros机器人运动控制,gazebo仿真
  • (附源码)springboot 房产中介系统 毕业设计 312341
  • (附源码)SSM环卫人员管理平台 计算机毕设36412
  • (九)c52学习之旅-定时器
  • (力扣记录)235. 二叉搜索树的最近公共祖先
  • (十六)一篇文章学会Java的常用API
  • (转)es进行聚合操作时提示Fielddata is disabled on text fields by default
  • (最优化理论与方法)第二章最优化所需基础知识-第三节:重要凸集举例
  • ***利用Ms05002溢出找“肉鸡
  • *p++,*(p++),*++p,(*p)++区别?
  • .【机器学习】隐马尔可夫模型(Hidden Markov Model,HMM)
  • .libPaths()设置包加载目录
  • .mysql secret在哪_MYSQL基本操作(上)
  • .net core Swagger 过滤部分Api
  • .net core使用ef 6
  • .net core使用RPC方式进行高效的HTTP服务访问
  • .NET gRPC 和RESTful简单对比
  • .NET 的程序集加载上下文
  • .NET 应用启用与禁用自动生成绑定重定向 (bindingRedirect),解决不同版本 dll 的依赖问题
  • .NET 自定义中间件 判断是否存在 AllowAnonymousAttribute 特性 来判断是否需要身份验证
  • .one4-V-XXXXXXXX勒索病毒数据怎么处理|数据解密恢复