内容简介:
作者简介
肖 玮
2016年至今一直在 arm 开源软件部门担任主任工程师,领导 Golang 针对 arm64 架构的功能实现(enabling)和性能优化工作,同时也是 Golang 汇编器(asm)和编译器(compile)针对 arm64 架构改进的主要贡献者之一。在加入 arm 之前一直供职于 Intel 开发者 工具 事业部,长期从事针对 X86 架构的动态二进制翻译器(DBT)和编译器产品等相关工作。
目
录
1.Toolchain
2.Compile
3.Asm
4.Link
5.Others
讲到 Arm 大家 首先会 想到 的就 是手机 的处理器, 不管是安卓还是苹果手机 ,他们所采用的处理器绝大部分都是由 Arm 设计 。那么 问题来了,为什么我会出现在一个 Go lang 的会议上 呢? 因为 Golang 看起来比较后端 。原因是 Arm 这几年除了继续聚焦于移动处理器,也开始在服务器市场发力。 一讲到服务器、云计算肯定少不了我们的 Golang。 因为移动计算的需求,Arm 处理器的功耗一直是很低的, 除此之外 Arm 服务器和英特尔服务器 有什么区别呢? 首先价格 便宜。 还有一些别的方面的差异, 例如 我 最近 远程登录过 一台 合作伙伴的 Arm 服务器 ,有两百多个核 。 以前一个服务如果要消耗一两百个核的话,需要好几台 X86 服务器来支撑, 现在 只需要一台 Arm 服务器就够了。 虽然 Arm 单核 性能没有 X86 好,但是省电,在性能要求不那么高的场合 ,例如存储服务 ,Arm 的服务器 就 很 有 优势。
1.Toolchain
1.1 Go toolchain overview
Golang 有好几 种 工具 链 。 绝大部分 用 户使用的工具链其实是 G C 工具 链 ,它 源 自于 plan9, 该系统虽然不是主流系统但 现在还 存 活着,有自己的一整套 工具 链,包括编译器、 汇编器和 链接器 等 等 。所以 GC 工具 链里有很多 plan9 的影子 。 第二个 工具 链就 是 gccg o, 它基于 Gcc 编译器工具链,对于一些比较老的架构例如 sparc 有很好的支持,其实 Golang 很多核心开发者以前就是 G CC 的开发者, 他们很钟爱以前的编译器, 所以肯定会让 GCC 支持 Go 语言 ,但 它 有一个问题,许可证 基 于 GPL ,这对一些开源项目可能会存在问题 。 第三个 工具链 llgo 是 基于 LLVM 的, 它 在 LLVM 上面做了一个 Golang 的前端,但是这个项目 现在 几乎没有人维护了。去年的时候,谷歌Go lang组 又起了一个 GoLLVM 的项目,试图走得更远一点 。 Golang 核心开发人员 现在对这个项目比较保守,一直说只是做实验, 短期 可能不会变成官方的 工具 链。
1.2 Go toolchain example
大家用的最多的可能是 Go build 或者是 Go inst all ,这 些工具 会调用一些其他的工具,比如 comp ile 和 asm。asm 就是做一些机器码的生成,最后把 各个包(package) 给 link 做一个拼装, 生成最终 可执行的代码。
1.3 Go toolchain workflow
上图所示, Go 的 工具 链除了刚才提的三 个工具 , 其实 还有 很多 别的 工具。比如有 cg o ,nm 等等。 还有 objdump ,就是进行反汇编,从机器码回到可读的 汇编 语言。 由于时间关系今天主要 给大家介 简单介绍一下 compile 、 asm 和 link 。 这些工具都是和目标机器的类型相关的, 如果你 的目标机器是 X86 而不是 Arm ,他们生成的 可执行 文件 是不一样的 。
2. Compile
2.1 Go compiler overview
我对 Go 编译器的理解分为经典的三个阶段:前端、中端和后端。
第一 个阶段 是 前端 , 用户的 Go 程序会被 前端 生成 抽象语法 树 (AST) , 并 AST 上面会做一些初步的工作 ,例如类型检查 。
第二 个阶段 是中 端 , Go 编译器形成跟目标无关的中间表示, 也就 是 基于 SSA 的中间表示 , 在这个 SSA 上面做 一些和目标机器无关的 优化会比较方便, 例如 一些图的 优化 会比较直观。
第三个阶段是后端, Go 编译器 会 生成 目标机器 的机器 指令 。
2.1.1 Front end
再回到前端, 这是用户最容易感受到的一个阶段,例如 如果你写的 Go 程序 不规范,语法上有问题, 就会被前端的 语法规则检查 报错 。 如果你表达是两边的类型写得不 匹配 ,前端 也 会 报错 。还有 inlie ,你的函数 比较小 ,你去 call 它的话不会有 call 指令, 前端会做 inline 优化,相当于 把它贴过来了。但是 inline 争议比较多,做的不太好 可能会有副作用 。 我最近看到谷歌的工程师在讨论这一点,他们会 在最近一两个版本 对这一块进行改进, 允许 一些非 叶子 函数做简单 的 inline。
2.1.2 Middle end
第二阶段 中端 跟 Go 语言关系不大,做 的也 是跟 目标 机器无关的事情,最常 做的 就 是 一些简单的 优化 。上图是 中端 现在支持的 所有 pass。 我们拿到一个函数, 后 要一个 pass,一个 pass 去做 遍历 ,把 没用的 节点和结构 删掉,或者把有用的信息 填 进去。 其实 有一个 pass :opt 现在做得 不是很好,不管你编译 Go 程序 时,优化 开关有 没有打开,其实都会运行 opt pass 。 现在 Golang 的调试体验不佳, 可能跟这个 pass 有点关系, 因为有 一些编译优化 是 你无法彻底关掉 的 。谈到调试的话 我注意到 谷歌 的工程师其实 在不断地改进用户体验,不管是我们 这里 说的 中 端 部分 还是最终的调试信息,他们一直都在 改进 。
举一个更详细的例子,比如说在 Go 语言写了一个整数除以 常 数, 但是 最终生成的指令 并没有真的做除法 ,而是 把除法 变成 了加减乘除的 简单运 算, 如上图所示, 保证左边和右边的结果一样 。 从 运行 速度 来看 , 不管是 X86 平台、Arm 平台 或者其他 的平台 ,都是 右边 的 更快 。 不管你 最终目标 机器 是什么体系结构 , 中端都会对这个常数除法表达式执行这个优化 。
2.1.3 Back end
第二阶段结束之后,我们 会 得到语义上完全一致的 SSA 提供给第三阶段: 后端 。 对于后端, 大家 最有感性认识的当属寄存器 分配 了 ,你 的程序 具体落地 到处理器上执行时 ,对于不同的处理器 ,其寄存器 数 和名字完全是不 一样 的 。
还有 机器指令 的选择。 还是以前面的常数 除法 为例,上图 左边是 机器 无关的 表示 ,右边是 Arm 64 目标机器上的最终后端生成的结果,注意 红色的那三条 指令。虽然中端的输出是一样的,但是 不同的 目标 机器到 后端 这里 得到的结果 就 会完全 不一样。
2.2 Generate prog
上面 Prog 这段 代码是 从 G olang 工具链 代码里面摘出来的, 描述了一条具体的 机器指令 。 我在这个“机器”上面加了引号, 因为它 是针对机器的汇编语言,离真正的机器指令还有一定的 差距 。 最后总结一下 编译器 的 三个阶段,前端处理跟语言相关的信息,中端处理跟机器无关的优化,后端处理跟机器有关的指令选择和 寄存 器的分配 。 顺便谈一下 和 Gcc 编译器 的 一些 不同 点 , 它 已经达到 2-300 个 pass ,而 Golang 编译 器只有几十个 pass,所以 Golang 编译器相对于Gcc 编译器还是很 弱 的 ,其实 这里 有很多 的 原因 。 一个原因就是谷歌可能 更 追求编译的速度,你做的事情越复杂,编译的速度越慢 。还有 二制代码 文件 的大小,有的时候要用空间换时间 ,例如 我可能为了在最终运行的时候快一点,要进行一些循环展开,就让你的循环 体 几倍的增长。 相比较于 G cc 编译器, Go 编译器到 目前为止比较高级的 编译 优化都没有做。
3.Asm
3.1 Go assembler overview
第二个工具是汇编器,对于 Golang 的汇编语言很多朋友可能用的不是特别多,除非一些性能要求比较高的场合,也许你会写 Golang 的汇编程序。Golang 的汇编语言来源于 plan9 的汇编语言。一条汇编指令对应一个 prog,前面我们讲过 编译器 可以直接 生成 prog ,如果手写汇编语言,就会经过比较经典的 词法和 语法分析 最后 构成一个 prog 的链。进到这个阶段,汇编器就要干活了 。 首先进行简单的优化,因为 Go lang 的汇编语言是个抽象的汇编语言,有一点接近高级语言 。除此之外还 有很多机器的信息,比如做一些 机器相关的 优化,还会做预处理, 会 选择最终的 指令 ,还会生成最终的 目标文件 ,就是跟运行 时 相关的语言信息和生成 meta 数据 ,最终生成我们说的 goobj 的文件 。
Go arm64 assembly example
这张图 描述了一段 非常简单的 Arm64 的汇编代码 。里面 做了一 个 加法 , 有一些 MOVD 指令,为什么会有这条指令呢? 我推测当初 p lan9 那帮人设计的Go lang 汇编 语言时 ,他们想做抽象 。 例如 我们做一个 MOVD 指令,真正落地 到各个体系结构 时 大家会发现既使是 MOVD 也有很多方式 。 对于 Arm 这种体系结构,MOV 能做两件事,要么进行无符号的扩展,要 么 进行有符号的扩展 。 但是 X86是不 同 ,MOVD 就比较麻烦了,要处理三个语义 。 最终由于体系结构的巨大差异 ,出现 了 很多 种类型的 MOVD 汇编语言指令,最后 plan9 不得不承认自己是一个准抽象的汇编语言。 对于 Arm, 如果你 将值 要 MOV 回到内存里面去, 汇编器会将 MOVD 翻译成 str,反之就是 ldr , 这些将汇编指令翻译成具体机器指令的事情 都是汇编器干的事。
Go lang 对每一个 Goroutine 的栈 大小是可变的,一开始 栈的大小是 2k , 随着 函数调用越来越 深 ,大概 消耗 到了 1K 多一点的时候, 栈 会进行动态的增长 。 怎么做这个动态的增长 呢? 汇编器 会在每个函数 开头插入 几条 指令 来检查栈的剩余空间大小 , 也就是查看 当前的栈 够 不够 用,不够用 就会跳到 下面去 。 下 面 就会 做函数调用,调用到 指定的 地方, 那里有 runtime 提供好 的函数进行内存管理,管理单纯的堆栈,把里面的指针 调好 。 有了足够多的栈空间, 这 时 你 的函数就 可以 做 任何 复杂的操作了 。 这是 Go 汇编器做简单事情的介绍,通过具体的例子让大家看起来比较直观。
总结 上述内容, Golang 的汇编语言翻译成最终的机器码,这些机器码在 Arm的机器上运行就可以按照设定好的规则进行计算或者读写内存。
3.2 Goobj
最后讲 讲 关于 Go lang 的 Goobj 形式 。大家都知道, Gcc 编译 C 文件也会生成 ELF obj 文件,他们的概念是一样的,但是千万不要用分析 ELF obj 文件格式的工具来 分析 Goobj 文件。如上图所示,Goobj 和 ELF obj 的文件结构 一开始 都 差不多,但在最前面有一些 header ,而 右边 是我们说的程序的表头,开始就不一样了 。 所以特别提醒大家对于 Go lang 生成的 obj 文件 要 用自己的工具去分析 。
汇编器 也 简单介绍完 了 , 它会 生成一些 goobj 格式的目标 文件。
4.Link
4.1 Go link overview
最后一个阶段是 链接: link 。link 大家 平时 用得更少并且 它 也是 Go lang 工具链 里面有很多瑕疵的地方 。
首先 介绍 一下基本概念,我们写了一个 Go 程序,有很多 Go 文件,可能会被编译器编成 目标文件 ,然后打包成 pkg,因为 Golang 是包 管理模式, 如果 Go程序调用 C 程序,工具链还 会调用 cg o, 生成额外的 C 文件, 并被本地的 编译器 处理 , 生成的目标文件 也会被打包成一个 pk g 文件,最后这些包的文件都会给我们的 链接 器, 生成最终的 可 执行程序 。
这个过程当中,我们连接器 link 干了很多 事情 ,你连接的模式是内部连接的时候,也有可能是偷懒什么都不干,就调用了本地的连接器,这个 过程和 Gcc的链接过程 没有什么太大的区别,都是 生成可执行 代码 文件 ,link 就是干这个事情的。但是 Golang 的 link 比 传统的 link 更加 复杂,复杂 的原因 在 于 Go 可以和C 混在一块用,就意味着 Go 的 link 既要处理自己的编译器生成的文件,还要负责你本地安装 的 C 语言工具链 生成的文件,这种情况下文件 格式 就有很多了, C 语言工具链 生成的 pk g 都要由我们的 Go link 来处理,这个是蛮难的, 而且这里面的 一些标准一直 都 在变。比如说今天写的 link 明天还是不是符合这个标准 都很难说 。 因此 Golang 工具链 偷懒了 , 就直接调用了外部的 link 做这个事情。
4.2 Go link workflow
上图是 我前段时间总结 的关于 Golang link 的 工作流程 。 一个正常 的链接器一般 都会做三件事情,首先把 各个部分都收集起来,然后是把 一些相似的部分挑出来 放在一块 , 例如将 代码 都 放在 text 段 。 最重要的就是 地址重定位 , 例如 A 函数调用 B 函数,但是 A 和 B 是在不同的文件里面,这个时候彼此不知道,A不知道 B 到底在哪里, 放到一块以后 , 链接 器知道 B 在第几个 字节 ,会把这个B 的地址写 到 A 的调用点 ,这 就是地址重定位 的过程。
Go 链接器 区别 传统链接器 在于 Go 语言,里面有很多 Golang 运行 时 的信息 。 例如 当我的栈 消耗 到一定阶段的时候,会被 Go 的 运行时 抢 走, 我 们先把栈拉大,里面 会 涉及比较复杂的问题,其中一个问题跟垃圾回收有关,我的指针原来指向的位置变了,要先找到指针的 位置 在哪里,这个 过程 有跟 G olang 的语言特性息息相关 , 这些东西 在传统的编译器里面不需要 考虑 。
链接器 最后 生成 每天都会用到的可执行文件 。 关于 Golang 链接器就 简单介绍 到这 ,如果大家对 里 面的细节有兴趣,欢迎线下讨论。
5.Others
最后一页分享 一些 我对 Go lang 比较热门话题 的看法。
1.VGo: Go 开发的程序越来越多, 但一些 老代码 可 能 还不能 轻易下 线,而其基于的第三方库又在不停的升, 。 这个时候版本控制 就 很有必要,所以 V Go += Package Versioning。
2.第二个 有意思 的事情 是去年有人 开始开发 基于 WebAssembly 的 后端 , 生成 出来的文件不 在 物理的 CPU 上 运行 , 而是 在浏览器上跑,这意味着 Go 语言可能也可以 开发前端应用 。
3. 关于安全点 。 Go 语言是自己做一些 垃 圾回收的, 函数刚 进 入 的时候 执行流可能 会被 调度器 抢掉 。这些都 是在函数的入口 。 如果你这个函数一直霸占着不放, 调度器 是无法抢占你的 CPU。 这点 JAVA 做 得 比较好, 即使一直在 循环 里 做 操作 , 调度器仍然能 抢占 你的 CPU ,Go 语言也想做这样,这样 的话 可调度性 会更好而且 垃圾回收 能 更加密集一点。
最后还有一些 跟体系结构相关的 优化 , 现在 函数调用的时候 Go ABI 不管是什么机器 都通过内存传参 , 但 Arm 的 寄存 器 非常多 ,这样可以考虑优化成寄存器传参, 究竟要不要打开这样的优化 有许多问题需要考虑,因为 一旦打开 ,首先 你调试体验就 会 变 得 很差,参数和指针找出来 也 变得麻烦 , 这一问题已经讨论一年多了,但还没有结论。 另外就是关于 inline,如果做得比较激进的话,性能会有比较大幅度的提升,但同样也会带来很多寄存器传参类似的问题 。
2018年的 Gopher Meetup 将在深圳开启巡回第一站,这一次邀请了很多新的讲师给大家一起交流分享Go的使用经验〜
点击 阅读原文 报名参加
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:- 【剖析 | SOFARPC 框架】系列之 SOFARPC 泛化调用实现剖析
- 剖析 SOFARPC 框架系列之 SOFARPC 泛化调用实现剖析
- RunTime实现原理剖析
- Docker 的实现原理剖析
- 剖析golang interface实现
- SOFAJRaft 线性一致读实现剖析 | SOFAJRaft 实现原理
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
深入理解TensorFlow:架构设计与实现原理
彭靖田、林健、白小龙 / 人民邮电出版社 / 2018-5-1 / 79.00元
本书以TensorFlow 1.2为基础,从基本概念、内部实现和实践等方面深入剖析了TensorFlow。书中首先介绍了TensorFlow设计目标、基本架构、环境准备和基础概念,接着重点介绍了以数据流图为核心的机器学习编程框架的设计原则与核心实现,紧接着还将TensorFlow与深度学习相结合,从理论基础和程序实现这两个方面系统介绍了CNN、GAN和RNN等经典模型,然后深入剖析了TensorF......一起来看看 《深入理解TensorFlow:架构设计与实现原理》 这本书的介绍吧!