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

【转发】https://segmentfault.com/a/1190000016743220

前言

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), AX0x0019 00025 (main.go:4)    PCDATA    $2, $00x0019 00025 (main.go:4)    MOVL    AX, (SP)0x001c 00028 (main.go:4)    PCDATA    $2, $10x001c 00028 (main.go:4)    LEAL    go.string."new "(SB), AX0x0022 00034 (main.go:4)    PCDATA    $2, $00x0022 00034 (main.go:4)    MOVL    AX, 4(SP)0x0026 00038 (main.go:4)    MOVL    $4, 8(SP)0x002e 00046 (main.go:4)    PCDATA    $2, $10x002e 00046 (main.go:4)    LEAL    go.string."hello"(SB), AX0x0034 00052 (main.go:4)    PCDATA    $2, $00x0034 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), AX0x001b 00027 (main.go:4)    PCDATA    $2, $00x001b 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 intfunc 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 +0x3aPrevious write at 0x00000112f470 by goroutine 5:main.add()/Users/sxs/Documents/go/src/test/main.go:15 +0x56Goroutine 6 (running) created at:main.main()/Users/sxs/Documents/go/src/test/main.go:11 +0x5aGoroutine 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/...

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/898442.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

根据空域图信息构造飞机航线图以及飞行轨迹模拟matlab仿真

1.程序功能描述 空域图是指航空领域中的一种图形表示方式,它涵盖了空中交通管理所需要的各种信息,比如航线、导航点、飞行高度层、飞行限制等。空域图是航空人员进行飞行计划制定的重要工具。在本课题中,根据空域图信息构造飞机航线图以及飞行轨迹模拟matlab仿真。 2.…

基于MobileNet深度学习网络的活体人脸识别检测算法matlab仿真

1.算法运行效果图预览 (完整程序运行后无水印) 2.算法运行软件版本 matlab2022a3.部分核心程序 (完整版代码包含详细中文注释和操作步骤视频)classs = 2; % 创建一个图像数据存储对象imdsTrain,用于从名为"Data"的文件夹及其子文件夹中读取图像数据。 % "…

2021年-PTA模拟赛-L1-8 编程团体赛(仅代码)

简单题,无解析没必要存进去再遍历数组,数组就起一个记忆的作用,我还没想到有什么优化的办法。AcCode: #include<bits/stdc++.h> using namespace std; int team[10010]; int main(){int winner = 0, max = 0, N;cin >> N;while(N--){int tid, id, g;scanf("…

[AI/GPT/综述] AI Agent的设计模式综述

【AI Agent】作为【AI应用层】的三大件(Prompt/RAG/Agent[MCP])之一, AI Agent的设计模式,作为未来或正在向AI开发转型的IT从业人员(开发人员/产品经理等),不得不深入研究研究。序:文由其一,随着大模型的发展,通用智能不断迭代升级,应用模式也不断创新,从简单的Prompt应…

2021年-PTA模拟赛-L1-7 整除光棍(C/C++思路)

除法竖式运算思路:在L1里面,那就不考虑大数运算了,列个竖式发现只需要每次得到除数之后输出,然后把余数乘10加1就可以进行下一轮运算了。 为什么说c/c++思路呢————java自带高精度运算,应该十行左右就可以搞定了。AcCode: #include<bits/stdc++.h> using namespac…

2021年-PTA模拟赛-L1-7 整除光棍(思路)

除法竖式运算思路:在L1里面,那就不考虑大数运算了,列个竖式发现只需要每次得到除数之后输出,然后把余数乘10加1就可以进行下一轮运算了。AcCode: #include<bits/stdc++.h> using namespace std; int main(){int x, t = 0, cnt = 0;cin >> x;while(t < x) t =…

记住密码和访问外部链接的实现

记住密码和访问外部链接的实现 今天的开发中实现了两项功能一个是记住密码,另外一个是可以访问外部链接. 记住密码 这个功能要使用到缓存技术,storage,uni中有自己的uni.stotageSync是同步的缓存技术,在登录成功跳转页面前把密码和用户名直接存储到缓存中, 在页面加载完成时在o…

Netty基础—4.NIO的使用简介

大纲 1.Buffer缓冲区 2.Channel通道 3.BIO编程 4.伪异步IO编程 5.改造程序以支持长连接 6.NIO三大核心组件 7.NIO服务端的创建流程 8.NIO客户端的创建流程 9.NIO优点总结 10.NIO问题总结1.Buffer缓冲区 (1)Buffer缓冲区的作用 (2)Buffer缓冲区的4个核心概念 (3)使用Direct模式创…

第二章 感知机

感知器模型数学理论 感知器(Perceptron)是一种二分类的线性分类模型,其输入为实例的特征向量,输出为实例的类别(取 +1 和 -1)。 模型定义 给定一个输入向量 \(\mathbf{x} = (x_1, x_2, \cdots, x_n)^T\),感知器模型的输出 \(y\) 由以下公式计算: \[y = \text{sign}(\ma…

国产操作系统为何被称为“矛盾综合体”

不知不觉我们已经使用了足足两个月的深度系统,同时还用虚拟机体验了开放麒麟,并收到了来自基层一线国产硬件系统办公用户的投稿,而且本系列文章在电脑报、壹零社等新媒体平台也饱受读者朋友关注,话题讨论度持续走高。 而在翻看了大家的留言之后,我们发现其实国产操作系统堪…

matplotlib常用方法

目录安装库创建图画流程1. 创建画板2. 创建一个个在画板上的图形轴3. 开始在图上进行画画线图plot散点图scatter条形图bar、barh直方图hist饼图pie 安装库 pip install matplotlib创建图画流程 1. 创建画板 在任何绘图之前,我们需要一个Figure对象,可以理解成我们需要一张画板…