关于编译器:CS-496-Homework-Assignment-3

CS 496: Homework Assignment 3 Due: 25 February, 11:55pm1 Assignment PoliciesCollaboration Policy. It is acceptable for students to collaborate in understanding the material but not in solving the problems or programming. Use of the Internet is allowed, but should not include searching for existing solutions.Under absolutely no circumstances code can be exchanged between students from different teams. Excerpts of code presented in class can be used.Assignments from previous offerings of the course must not be re-used. Viola- tions will be penalized appropriately.2 AssignmentThis assignment consists in implementing a series of extensions to the interpreter for the language called LET that we saw in class. The concrete syntax of the extensions, the abstract syntax of the extensions (ast.ml) and the parser that converts the concrete syntax into the abstract syntax is already provided for you. Your task is to complete the definition of the interpreter, that is, the function eval_expr so that it is capable of handling the new language features.Before addressing the extensions, we briefly recall the concrete and abstract syntax of LET. The concrete syntax is given by the grammar in Fig. 1. Each line in this grammar is called a production of the grammar. We will be adding new productions to this grammar corresponding to the extensions of LET that we shall study. These shall be presented in Section 3.Next we recall the abstract syntax of LET, as presented in class. We shall also be extending this syntax with new cases for the new language features that we shall add to LET.1 ...

February 18, 2024 · 6 min · jiezi

关于编译器:开源项目分享实习宝典传授直播课程报名开启

你是否须要 AI 初学者入门级的开源教程? 你是否期待和顶尖开发者一起学习,向深度学习畛域的大佬看齐? 你是否心愿通过课程解说,理解我的项目实际,把握深度学习、大模型相干的前沿 AI 技术? 如果你的答案是必定的,那么 「MegEngine 开发者说」系列课程正是你想要的! 「MegEngine 开发者说」是由旷视天元(MegEngine)官网推出的线上课程,由不同行业畛域、不同 AI 工龄的优良开源我的项目开发者进行前沿技术、我的项目教训分享,更有实习、找工作、保研、较量等满满干货,助力正处于不同阶段的开发者学习技术、升学、升职! 第一期重磅来袭!本期课程咱们邀请到了两位优良开发者进行主题分享: 邱忠喜-北方科技大学 善于畛域:智能医疗影像畛域演讲主题:SAM-集体想法与瞻望亮点领先看:从 SAM 简要介绍,到一些基于 SAM 的利用与钻研算法,SAM 时代咱们该何去何从,分享 SAM 带来的思考与启发。李成远-中国人民大学 善于畛域:AI 模型推理减速演讲主题:基于 MegCC 的模型推理优化教训分享亮点领先看:从开源我的项目介绍到实习经验分享,深度学习模型编译器 MegCC 介绍及 ONNX 接入 MegCC,更有多家大厂实习经验,为在校开发者传授实习宝典。课程工夫:9 月 9 日(周六)晚 19:00-20:00 填写报名问卷并参加直播,更有直播间有奖互动答疑环节,精美的 MegEngine 周边礼品等你来拿! 背景介绍: 开源深度学习框架旷视天元(MegEngine)是旷视自主研发的国产工业级深度学习框架,是旷视新一代AI生产力平台Brain++的最外围组件,在2020年3月正式向寰球开发者开源。MegEngine 凭借其训练推理一体、超低硬件门槛和全平台高效推理 3 大外围劣势,可能帮忙企业与开发者大幅节俭产品从实验室原型到工业部署的流程,真正实现小时级的转化能力。 更多 MegEngine 信息获取,您能够退出 MegEngine 用户交换 QQ 群:1029741705。欢送参加 MegEngine 社区奉献,成为 Awesome MegEngineer,荣誉证书、定制礼品享不停。

August 26, 2023 · 1 min · jiezi

关于编译器:编译-TFLite-模型

本篇文章译自英文文档 Compile TFLite Models 作者是 FrozenGene (Zhao Wu) · GitHub更多 TVM 中文文档可拜访 →Apache TVM 是一个端到端的深度学习编译框架,实用于 CPU、GPU 和各种机器学习减速芯片。 | Apache TVM 中文站 本文介绍如何用 Relay 部署 TFLite 模型。 首先装置 TFLite 包。 # 装置 tflitepip install tflite==2.1.0 --user或者自行生成 TFLite 包,步骤如下: # 获取 flatc 编译器。# 具体可参考 https://github.com/google/flatbuffers,确保正确装置flatc --version# 获取 TFLite 架构wget https://raw.githubusercontent.com/tensorflow/tensorflow/r1.13/tensorflow/lite/schema/schema.fbs# 生成 TFLite 包flatc --python schema.fbs# 将以后文件夹门路(蕴含生成的 TFLite 模块)增加到 PYTHONPATH。export PYTHONPATH=${PYTHONPATH:+$PYTHONPATH:}$(pwd)用 python -c "import tflite" 命令,查看 TFLite 包是否装置胜利。 无关如何用 TVM 编译 TFLite 模型的示例如下: ...

June 19, 2023 · 2 min · jiezi

关于编译器:MegEngine-使用小技巧如何解读-MegCC-编译模型几个阶段-Pass-的作用

MegCC 是一个真真实实的深度学习模型编译器,具备极其轻量的 Runtime 二进制体积,高性能,不便移植,极低内存应用以及快启动等外围特点。用户可在 MLIR 上进行计算图优化,内存布局,最初通过事后写好的 code 模版进行代码生成。 MegCC 中次要的 Pass MGBToKernelPass:这个 Pass 次要将 MGB IR 转换为 Abstract Kernel IR,转换过程中次要实现几件事件: 将 MGB IR 中的所有输入输出 Tensor 类型转换为 Buffer 类型。将 MGB IR 中的所有枚举参数转换为对应的字符,这样 Abstract Kernel IR 就能够齐全和 MegEngine 解耦。将一些内存搬运相干的 Opr 全副转换为 Relayout,如:Concat,SetSubtensor 等 Opr(node-level optimizations)。将判断 Opr 是动态 shape 还是动静 shape,动静 shape 就是输出 tensor 的 shape 须要依赖输出的值能力计算出来的,如:输入一个 tensor 中所有大于 1 的数。如果是动态 shape 间接转换到 Abstract Kernel IR,如果是动静 shape 间接转换到 Kernel IR 的 Instruction 中。MGBFuseKernelPass:利用在 MGB IR 上,基于 mlir 的模板匹配的办法尽可能的实现 kernel 的交融,比方间断两个 typecvt 合并成为一个 typecvt 等(block-level optimizations,算子交融)。MemoryForwardingPass:将遍历 Abstract Kernel IR 所有可能不必计算,间接 share 输出内存的 Opr,如果这些 Opr 的确不必计算,则间接 forward memory,如果这些 Opr 须要进行内存搬运,则会用 Relayout Opr 替换原来的 Opr(node-level optimizations)。KernelMaterializationPass:将所有 Abstract Kernel IR 都装载上真正 Kernel code 并转化为 KernelCall,而后增加对应的 KernelDef。KernelCall 和 KernelDef 之间通过 symbol 进行匹配。StaticMemoryPlanningPass:将所有动态 shape 的 memref 进行内存布局,内存布局算法应用改良的 MegEngine 的内存布局算法--PushDown 算法,可能极大水平的压缩运行时内存使用量。同时将 mlir 的 memref.Alloc 替换为 Kernel IR 的 MemPlan,MemPlan 中次要记录了内存布局的一整块 memref 以及该 Tensor 在布局的内存中的偏移量(dataflow-level optimizations,动态内存布局)。下面的 Pass 就实现模型的图优化、内存布局以及 Kernel 生成,上文提到的后端优化即在 Kernel 生成阶段体现,目前 MegCC 次要应用人工优化的 Kernel 模版。最终能够依据 Runtime 中定义的模型格局 dump 编译之后的模型,以及生成计算模型所需的 Kernel 文件。 上面以一个简略的模型为例,应用 MegCC 的辅助工具(下载 Release 包) mgb-importer 和 megcc-opt,察看通过各个 Pass 的解决 IR 的变动。也可应用 mgb-to-tinynn 工具间接实现模型的编译过程,详见 MegCC 入门文档。 ...

May 30, 2023 · 6 min · jiezi

关于编译器:终于放弃了使用5年的Markdown笔记编辑器

问大家一个问题:你最罕用的笔记软件是哪一款? 想必大家可能列举很多笔记软件,印象笔记、有道云笔记、为知笔记、语雀.... 我最喜爱用的是Typora,目前曾经应用超过5年,一款口碑和应用体验俱佳的Markdown编辑器。 然而,本文的配角并不是Typora。 Typora很好,然而它也有很多弊病,比方团队协同、跨设施应用。 换句话说,Typora是一款实用于单机应用,场景比拟繁多的笔记软件。 而且,自2021年11月23日起公布1.0版本,开始成为免费软件。 明天要给大家介绍的这款工具名字叫Eraser,一款实用于团队合作的多合一构思工具。 Eraser容许用户应用markdown笔记编辑器、轻量级画布以思维导图进行迅速交换想法。 应用Eraser进行头脑风暴、图表绘制、线框设计等场景! Eraser是一个为近程合作而建设的可视化画布,它是轻量级的,无烦扰的,所以你能够专一于实现工作。 另外,Eraser是一款网页端工具,因而,不须要下载客户端,就能够在Windows、macOS、Linux、Android、iOS等设施下应用。 注册账号,登录Eraser网页端之后,会发现它有两个外围性能,能够应用Markdown作笔记,也能够应用画布绘制流程图。 笔记方面,它反对代码块、行内代码、题目、加粗等罕用的Markdown语法。 画布方面,能够反对绘制架构图、实体关系图、仪表盘等。 而且,它内置了很多罕用的模板,能够在这些模板的根底上疾速绘制想要的图形。 此外,它还有一点比拟吸引我的性能,就是能够间接导出为PDF文档。 这对于Markdown是一个十分亟待解决的问题,Markdown尽管应用起来十分不便,然而分享却是一个大问题,有一些工具须要借助插件能力实现PDF导出,然而格局和复杂度都能劝退很多人。 在这方面,Eraser也是十分吸引人的。 最初,Eraser目前完全免费,感兴趣的同学能够尝试一下。

May 27, 2022 · 1 min · jiezi

编译器Recursive-descent-parser分析

原文地址:编译器Recursive descent parser分析Introduction对给出的编程语法,用Java分析Recursive descent parser. Problem 1Write a recursive descent parser for the language generated by the grammar: S → → E_LISTE LIST → EXPR E_TAILE TAIL → → E_LIST EXPR → S_EXPRS_EXPR → ANDOP S_TAILS_TAIL → → ′|′ S EXPR ANDOP → RELOP A_TAILA_TAIL → → ′&′ ANDOP RELOP → TERM R_TAILR_TAIL → → ′<′ RELOP → ′>′ RELOP → ′=′ RELOP → ′#′ RELOP TERM → FACT T_TAILT_TAIL → → ′+′ TERM → ′−′ TERM FACT → VALUE F_TAILF_TAIL → → ′∗′ FACT → ′/′ FACT VALUE → LIST → UNARY → LITERAL → ′(′EXPR′)′ → SYMBOL LIST → ′[′ARGS′]′ UNARY → ′−′ VALUE → ′!′ VALUE ARGS → → EXPR A_LISTA_LIST → → ′,′ EXPRA_LISTSYMBOL → symbolLITERAL→ integer → string → ′true′ → ′false′ → ′nil′The terminal int denotes an integer, string denotes a double quoted string, e.g., "hello world" and the terminal symbol denotes a symbol, e.g., myVar. ...

August 28, 2019 · 3 min · jiezi

C++编译器优化

1、volatile:易变性:volatile告诉编译器,某个变量是易变的,当编译器遇到这个变量的时候,只能从变量的内存地址中读取这个变量,不可以从缓存、寄存器、或者其它 任何地方读取。顺序性:两个包含volatile变量的指令,编译后不可以乱序。注意是编译后不乱序,但是在执行的过程中还是可能会乱序的,这点需要由其它机制来保证,例如memory- barriers。不可优化性:volatile告诉编译器,不要对这个变量进行各种激进的优化,甚至将变量直接消除,保证代码中的指令一定会被执行。2、NRV(Named Return Value)优化:函数返回一个类,例如下:class X;X bar(){X x1;// 处理 x1..return x1;}编译器实现:// 函数实现void bar(X& __result) // 加上一个额外参数{// 预留x1的内存空间X x1;// 编译器产生的默认构造函数的调用,x1.X::X();// 处理 x1..// 编译器产生的拷贝操作__result.X::X(x1);return;}// 函数调用X x2; // 这里只是预留内存,并未调用初始化函数bar(x2);NRV优化后:void bar(X& __result){// 调用__result的默认构造函数__result.X::X();// 处理__resultreturn;}3、循环内变量优化:void test2(char s);void test(){ int i; for (i = 0; i < 10; i ++) {char buf[256];test2(buf); //调用test2是为了让编译器认为buf有用,以免被优化掉}}汇编代码:movl $10, %ebxsubl $272, %esp #分配272字节栈空间leal -264(%ebp), %esi #取buf地址.L2:movl %esi, (%esp) #buf地址入栈call test2 #调用test2subl $1, %ebxjne .L2 #循环未结束则跳到L2该函数中,buf不会每次循环都生成,而是循环外生成,循环内不断的使用。4、算数式优化a2被编译成a+a;无符号数a/2被编译成a>>1;有符号数a/2。5、memset函数优化memset函数常用来初始化大段内存,但对小数据来说memset能否保持足够高效呢?看这段程序:编译成汇编:movl $0, -24(%ebp) #设置s1movl $0, -20(%ebp)movl $0, -16(%ebp)movl $0, -12(%ebp)call test2 #调用test2leal -8216(%ebp), %edx #设置s2xorl %eax, %eaxmovl %edx, %edimovl $2048, %ecxrep stoslmovl %edx, (%esp) #调用test2call test2movl %ebx, (%esp) #设置s3movl $8193, 8(%esp)movl $0, 4(%esp)call memsetmovl %ebx, (%esp) #调用test2call test2当数据长度比较小时(如s1是16字节),memset被编译成连续的赋值语句;当数据长度不大于8KB时(如s2),memset用串操作指令来实现;当数据长度大于8KB时(如s3),memset被编译成函数调用。串操作类指令:在内存一个存储区域连续存放着若干个字节(或字)数据,这样一组数据称为“数据串”(高级语言视为数组)。若每个数据是一个字节,称“字节串”;若是字,则称“字串”。串操作指令可以用来实现内存区域的数据串操作。串操作指令每次只处理数据串中的一个数据,但与重复前缀配合使用(重复前缀+串操作指令),则可使操作重复进行(其执行过程相当于一个循环程序的运行,重复次数由寄存器CX决定)。 ...

February 17, 2019 · 1 min · jiezi

Go 语言编译过程概述

Golang 是一门需要编译才能运行的编程语言,也就说代码在运行之前需要通过编译器生成二进制机器码,随后二进制文件才能在目标机器上运行,如果我们想要了解 Go 语言的实现原理,理解它的编译过程就是一个没有办法绕过的事情。这一节会先对 Go 语言编译的过程进行概述,从顶层介绍编译器执行的几个步骤,随后的章节会分别剖析各个步骤完成的工作和实现原理,同时也会对一些需要预先掌握的知识进行介绍和准备,确保后面的章节能够被更好的理解。目录编译原理概述词法和语法分析器类型检查中间代码生成机器码生成预备知识想要深入了解 Go 语言的编译过程,需要提前了解一下编译过程中涉及的一些术语和专业知识。这些知识其实在我们的日常工作和学习中比较难用到,但是对于理解编译的过程和原理还是非常重要的。这一小节会简单挑选几个常见并且重要的概念提前进行介绍,减少后面章节的理解压力。抽象语法树抽象语法树(AST)是源代码语法的结构的一种抽象表示,它用树状的方式表示编程语言的语法结构。抽象语法树中的每一个节点都表示源代码中的一个元素,每一颗子树都表示一个语法元素,例如一个 if else 语句,我们可以从 2 * 3 + 7 这一表达式中解析出下图所示的抽象语法树。作为编译器常用的数据结构,抽象语法树抹去了源代码中不重要的一些字符 - 空格、分号或者括号等等。编译器在执行完语法分析之后会输出一个抽象语法树,这棵树会辅助编译器进行语义分析,我们可以用它来确定结构正确的程序是否存在一些类型不匹配或不一致的问题。静态单赋值静态单赋值(SSA)是中间代码的一个特性,如果一个中间代码具有静态单赋值的特性,那么每个变量就只会被赋值一次,在实践中我们通常会用添加下标的方式实现每个变量只能被赋值一次的特性,这里以下面的代码举一个简单的例子:x := 1x := 2y := x根据分析,我们其实能够发现上述的代码其实并不需要第一个将 1 赋值给 x 的表达式,也就是这一表达式在整个代码片段中是没有作用的:x1 := 1x2 := 2y1 := x2从使用 SSA 的『中间代码』我们就可以非常清晰地看出变量 y1 的值和 x1 是完全没有任何关系的,所以在机器码生成时其实就可以省略第一步,这样就能减少需要执行的指令来优化这一段代码。根据 Wikipedia 对 SSA 的介绍来看,在中间代码中使用 SSA 的特性能够为整个程序实现以下的优化:常数传播(constant propagation)值域传播(value range propagation)稀疏有条件的常数传播(sparse conditional constant propagation)消除无用的程式码(dead code elimination)全域数值编号(global value numbering)消除部分的冗余(partial redundancy elimination)强度折减(strength reduction)寄存器分配(register allocation)从 SSA 的作用我们就能看出,因为它的主要作用就是代码的优化,所以是编译器后端(主要负责目标代码的优化和生成)的一部分;当然,除了 SSA 之外代码编译领域还有非常多的中间代码优化方法,优化编译器生成的代码是一个非常古老并且复杂的领域,这里就不会展开介绍了。指令集架构最后要介绍的一个预备知识就是指令集的架构了,很多开发者都会遇到在生产环境运行的结果和本地不同的问题,导致这种情况的原因其实非常复杂,不同机器使用不同的指令也是可能的原因之一。我们大多数开发者都会使用 x86_64 的 Macbook 作为工作上主要使用的硬件,在命令行中输入 uname -m 就能够获得当前机器上硬件的信息:$ uname -mx86_64x86_64 是目前比较常见的指令集架构之一,除了 x86_64 之外,还包含其他类型的指令集架构,例如 amd64、arm64 以及 mips 等等,不同的处理器使用了大不相同的机器语言,所以很多编程语言为了在不同的机器上运行需要将源代码根据架构翻译成不同的机器代码。复杂指令集计算机(CISC)和精简指令集计算机(RISC)是目前的两种 CPU 区别,它们的在设计理念上会有一些不同,从名字我们就能看出来这两种不同的设计有什么区别,复杂指令集通过增加指令的数量减少需要执行的质量数,而精简指令集能使用更少的指令完成目标的计算任务;早期的 CPU 为了减少机器语言指令的数量使用复杂指令集完成计算任务,这两者之前的区别其实就是设计上的权衡,我们会在后面的章节 机器码生成 中详细介绍指令集架构,当然各位读者也可以自行搜索和学习。编译原理Go 语言编译器的源代码在 cmd/compile 目录中,目录下的文件共同构成了 Go 语言的编译器,学过编译原理的人可能听说过编译器的前端和后端,编译器的前端一般承担着词法分析、语法分析、类型检查和中间代码生成几部分工作,而编译器后端主要负责目标代码的生成和优化,也就是将中间代码翻译成目标『机器』能够运行的机器码。Go 的编译器在逻辑上可以被分成四个阶段:词法与语法分析、类型检查和 AST 转换、通用 SSA 生成和最后的机器代码生成,在这一节我们会使用比较少的篇幅分别介绍这四个阶段做的工作,后面的章节会具体介绍每一个阶段的具体内容。词法与语法分析所有的编译过程其实都是从解析代码的源文件开始的,词法分析的作用就是解析源代码文件,它将文件中的字符串序列转换成 Token 序列,方便后面的处理和解析,我们一般会把执行词法分析的程序称为词法解析器(lexer)。而语法分析的输入就是词法分析器输出的 Token 序列,这些序列会按照顺序被语法分析器进行解析,语法的解析过程就是将词法分析生成的 Token 按照语言定义好的文法(Grammar)自下而上或者自上而下的进行规约,每一个 Go 的源代码文件最终会被归纳成一个 SourceFile 结构:SourceFile = PackageClause “;” { ImportDecl “;” } { TopLevelDecl “;” } .标准的 Golang 语法解析器使用的就是 LALR(1) 的文法,语法解析的结果其实就是上面介绍过的抽象语法树(AST),每一个 AST 都对应着一个单独的 Go 语言文件,这个抽象语法树中包括当前文件属于的包名、定义的常量、结构体和函数等。如果在语法解析的过程中发生了任何语法错误,都会被语法解析器发现并将消息打印到标准输出上,整个编译过程也会随着错误的出现而被中止。我们会在这一章后面的小节 词法与语法分析 中介绍 Go 语言的文法和它的词法与语法解析过程。类型检查当拿到一组文件的抽象语法树 AST 之后,Go 语言的编译器会对语法树中定义和使用的类型进行检查,类型检查分别会按照顺序对不同类型的节点进行验证,按照以下的顺序进行处理:常量、类型和函数名及类型;变量的赋值和初始化;函数和闭包的主体;哈希键值对的类型;导入函数体;外部的声明;通过对每一棵抽象节点树的遍历,我们在每一个节点上都会对当前子树的类型进行验证保证当前节点上不会出现类型错误的问题,所有的类型错误和不匹配都会在这一个阶段被发现和暴露出来。类型检查的阶段不止会对树状结构的节点进行验证,同时也会对一些内建的函数进行展开和改写,例如 make 关键字在这个阶段会根据子树的结构被替换成 makeslice 或者 makechan 等函数。我们其实能够看出类型检查不止做了验证类型的工作,还做了对 AST 进行改写,处理 Go 语言内置关键字的活,所以,这一过程在整个编译流程中还是非常重要的,没有这个步骤很多关键字其实就没有办法工作,后面的章节 类型检查 会介绍这一步骤。中间代码生成当我们将源文件转换成了抽象语法树、对整棵树的语法进行解析并进行类型检查之后,就可以认为当前文件中的代码基本上不存在无法编译或者语法错误的问题了,Go 语言的编译器就会将输入的 AST 转换成中间代码。Go 语言编译器的中间代码使用了 SSA(Static Single Assignment Form) 的特性,如果我们在中间代码生成的过程中使用这种特性,就能够比较容易的分析出代码中的无用变量和片段并对代码进行优化。在类型检查之后,就会通过一个名为 compileFunctions 的函数开始对整个 Go 语言项目中的全部函数进行编译,这些函数会在一个编译队列中等待几个后端工作协程的消费,这些 Goroutine 会将所有函数对应的 AST 转换成使用 SSA 特性的中间代码。中间代码生成 这一章节会详细介绍中间代码的生成过程并简单介绍 Golang 是如何在中间代码中使用 SSA 的特性的,在这里就不展开介绍其他的内容了。机器码生成Go 语言源代码的 cmd/compile/internal 中包含了非常多机器码生成相关的包,不同类型的 CPU 分别使用了不同的包进行生成 amd64、arm、arm64、mips、mips64、ppc64、s390x、x86 和 wasm,也就是说 Go 语言能够在上述的 CPU 指令集类型上运行,其中比较有趣的就是 WebAssembly 了。作为一种在栈虚拟机上使用的二进制指令格式,它的设计的主要目标就是在 Web 浏览器上提供一种具有高可移植性的目标语言。Go 语言的编译器既然能够生成 WASM 格式的指令,那么就能够运行在常见的主流浏览器中。$ GOARCH=wasm GOOS=js go build -o lib.wasm main.go我们可以使用上述的命令将 Go 的源代码编译成能够在浏览器上运行的『汇编语言』,除了这种新兴的指令之外,Go 语言还支持了几乎全部常见的 CPU 指令集类型,也就是说它编译出的机器码能够在使用上述指令集的机器上运行。机器码生成 一节会详细介绍将中间代码翻译到不同目标机器的过程,在这个章节中也会简单介绍不同的指令集架构的区别。编译器入口Go 语言的编译器入口在 src/cmd/compile/internal/pc 包中的 main.go 文件,这个 600 多行的 Main 函数就是 Go 语言编译器的主程序,这个函数会先获取命令行传入的参数并更新编译的选项和配置,随后就会开始运行 parseFiles 函数对输入的所有文件进行词法与语法分析得到文件对应的抽象语法树:func Main(archInit func(*Arch)) { // … lines := parseFiles(flag.Args())接下来就会分九个阶段对抽象语法树进行更新和编译,就像我们在上面介绍的,整个过程会经历类型检查、SSA 中间代码生成以及机器码生成三个部分:检查常量、类型和函数的类型;处理变量的赋值;对函数的主体进行类型检查;决定如何捕获变量;检查内联函数的类型;进行逃逸分析;将闭包的主体转换成引用的捕获变量;编译顶层函数;检查外部依赖的声明;了解了剩下的编译过程之后,我们重新回到词法和语法分析后的具体流程,在这里编译器会对生成语法树中的节点执行类型检查,除了常量、类型和函数这些顶层声明之外,它还会对变量的赋值语句、函数主体等结构进行检查: for i := 0; i < len(xtop); i++ { n := xtop[i] if op := n.Op; op != ODCL && op != OAS && op != OAS2 && (op != ODCLTYPE || !n.Left.Name.Param.Alias) { xtop[i] = typecheck(n, ctxStmt) } } for i := 0; i < len(xtop); i++ { n := xtop[i] if op := n.Op; op == ODCL || op == OAS || op == OAS2 || op == ODCLTYPE && n.Left.Name.Param.Alias { xtop[i] = typecheck(n, ctxStmt) } } for i := 0; i < len(xtop); i++ { n := xtop[i] if op := n.Op; op == ODCLFUNC || op == OCLOSURE { typecheckslice(Curfn.Nbody.Slice(), ctxStmt) } } checkMapKeys() for _, n := range xtop { if n.Op == ODCLFUNC && n.Func.Closure != nil { capturevars(n) } } escapes(xtop) for _, n := range xtop { if n.Op == ODCLFUNC && n.Func.Closure != nil { transformclosure(n) } }类型检查会对传入节点的子节点进行遍历,这个过程会对 make 等关键字进行展开和重写,类型检查结束之后并没有输出新的数据结构,只是改变了语法树中的一些节点,同时这个过程的结束也意味着源代码中已经不存在语法错误和类型错误,中间代码和机器码也都可以正常的生成了。 initssaconfig() peekitabs() for i := 0; i < len(xtop); i++ { n := xtop[i] if n.Op == ODCLFUNC { funccompile(n) } } compileFunctions() for i, n := range externdcl { if n.Op == ONAME { externdcl[i] = typecheck(externdcl[i], ctxExpr) } } checkMapKeys()}在主程序运行的最后,会将顶层的函数编译成中间代码并根据目标的 CPU 架构生成机器码,不过这里其实也可能会再次对外部依赖进行类型检查以验证正确性。总结Go 语言的编译过程其实是非常有趣并且值得学习的,通过对 Go 语言四个编译阶段的分析和对编译器主函数的梳理,我们能够对 Golang 的实现有一些基本的理解,掌握编译的过程之后,Go 语言对于我们来讲也不再是一个黑盒,所以学习其编译原理的过程还是非常让人着迷的。相关文章编译原理概述词法和语法分析器类型检查中间代码生成机器码生成ReferenceIntroduction to the Go compilerGo 1.5 Bootstrap PlanGo grammar questiowhat type of grammar GO programming language? ...

February 11, 2019 · 3 min · jiezi

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:noinlinenoinline 顾名思义,不要内联。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:noinlinefunc 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:nosplitnosplit 的作用是:跳过栈溢出检测。栈溢出是什么?正是因为一个 Goroutine 的起始栈大小是有限制的,且比较小的,才可以做到支持并发很多 Goroutine,并高效调度。stack.go 源码中可以看到,_StackMin 是 2048 字节,也就是 2k,它不是一成不变的,当不够用时,它会动态地增长。那么,必然有一个检测的机制,来保证可以及时地知道栈不够用了,然后再去增长。回到话题,nosplit 就是将这个跳过这个机制。优劣显然地,不执行栈溢出检查,可以提高性能,但同时也有可能发生 stack overflow 而导致编译失败。//go:noescapenoescape 的作用是:禁止逃逸,而且它必须指示一个只有声明没有主体的函数。逃逸是什么?Go 相比 C、C++ 是内存更为安全的语言,主要一个点就体现在它可以自动地将超出自身生命周期的变量,从函数栈转移到堆中,逃逸就是指这种行为。请参考我之前的文章,逃逸分析。优劣最显而易见的好处是,GC 压力变小了。因为它已经告诉编译器,下面的函数无论如何都不会逃逸,那么当函数返回时,其中的资源也会一并都被销毁。不过,这么做代表会绕过编译器的逃逸检查,一旦进入运行时,就有可能导致严重的错误及后果。//go:noracenorace 的作用是:跳过竞态检测我们知道,在多线程程序中,难免会出现数据竞争,正常情况下,当编译器检测到有数据竞争,就会给出提示。如:var sum intfunc main() { go add() go add()}func add() { sum++}执行 go run -race main.go 利用 -race 来使编译器报告数据竞争问题。你会看到:==================WARNING: DATA RACERead 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 语言的编译器指示,除非你确认你的程序的性能瓶颈在编译器上,否则你都应该先去关心其他更可能出现瓶颈的事情。 ...

October 21, 2018 · 2 min · jiezi

精读《手写 SQL 编译器 - 错误提示》

1 引言编译器除了生成语法树之外,还要在输入出现错误时给出恰当的提示。比如当用户输入 select (name,这是个未完成的 SQL 语句,我们的目标是提示出这个语句未完成,并给出后续的建议: ) - + % / * . ( 。2 精读分析一个 SQL 语句,现将 query 字符串转成 Token 数组,再构造文法树解析,那么可能出现错误的情况有两种:语句错误。文法未完成。给出错误提示的第一步是判断错误发生。通过这张 Token 匹配过程图可以发现,当深度优先遍历文法节点时,匹配成功后才会返回父元素继续往下走。而当走到父元素没有根节点了才算匹配成功;当尝试 Chance 时没有机会了,就是错误发生的时机。所以我们只要找到最后一个匹配成功的节点,再根据最后成功与否,以及搜索出下一个可能节点,就能知道错误类型以及给出建议了。function onMatchNode(matchNode, store) { const matchResult = matchNode.run(store.scanner); if (!matchResult.match) { tryChances(matchNode, store); } else { const restTokenCount = store.scanner.getRestTokenCount(); if (matchNode.matching.type !== “loose”) { if (!lastMatch) { lastMatch = { matchNode, token: matchResult.token, restTokenCount }; } } callParentNode(matchNode, store, matchResult.token); }}所以在运行语法分析器时,在遇到匹配节点(MatchNode)时,如果匹配成功,就记录下这个节点,这样我们最终会找到最后一个匹配成功的节点:lastMatch。之后通过 findNextMatchNodes 函数找到下一个可能的推荐节点列表,作为错误恢复的建议。findNextMatchNodes 函数会根据某个节点,找出下一节点所有可能 Tokens 列表,这个函数后面文章再专门介绍,或者你也可以先阅读 源码.语句错误也就是任何一个 Token 匹配失败。比如:select * from table_name as table1 error_string;这里 error_string 就是冗余的语句。通过语法解析器分析,可以得到执行失败的结果,然后通过 findNextMatchNodes 函数,我们可以得到下面分析结果:可以看到,程序判断出了 error_string 这个 Token 属于错误类型,同时给出建议,可以将 error_string 替换成这 14 个建议字符串中任意一个,都能使语句正确。之所以失败类型判断为错误类型,是因为查找了这个正确 Token table1 后面还有一个没有被使用的 error_string,所以错误归类是 wrong。注意,这里给出的是下一个 Token 建议,而不是全部 Token 建议,因此推荐了 where 表示 “或者后面跟一个完整的 where 语句”。文法未完成和语句错误不同,这种错误所有输入的单词都是正确的,但却没有写完。比如:select *通过语法解析器分析,可以得到执行失败的结果,然后通过 findNextMatchNodes 函数,我们可以得到下面分析结果:可以看到,程序判断出了 * 这个 Token 属于未完成的错误类型,建议在后面补全这 14 个建议字符串中任意一个。比较容易联想到的是 where,但也可以是任意子文法的未完成状态,比如后面补充 , 继续填写字段,或者直接跟一个单词表示别名,或者先输入 as 再跟别名。之所以失败类型判断为未完成,是因为最后一个正确 Token * 之后没有 Token 了,但语句解析失败,那只有一个原因,就是语句为写完,因此错误归类是 inComplete。找到最易读的错误类型在一开始有提到,我们只要找到最后一个匹配成功的节点,就可以顺藤摸瓜找到错误原因以及提示,但最后一个成功的节点可能和我们人类直觉相违背。举下面这个例子:select a from b where a = ‘1’ ~ – 这里手滑了正常情况,我们都认为错误点在 ~,而最后一个正确输入是 ‘1’。但词法解析器可不这么想,在我初版代码里,判断出错误是这样的:提示是 where 错了,而且提示是 .,有点摸不着头脑。读者可能已经想到了,这个问题与文法结构有关,我们看 fromClause 的文法描述:const fromClause = () => chain( “from”, tableSources, optional(whereStatement), optional(groupByStatement), optional(havingStatement) )();虽然实际传入的 where 语句多了一个 ~ 符号,但由于文法认为整个 whereStatement 是可选的,因此出错后会跳出,跳到 b 的位置继续匹配,而 显然 groupByStatement 与 havingStatement 都不能匹配到 where,因此编译器认为 “不会从 b where a = ‘1’ ~” 开始就有问题吧?因此继续往回追溯,从 tableName 开始匹配:const tableName = () => chain([matchWord, chain(matchWord, “.”, matchWord)()])();此时第一次走的 b where a = ‘1’ ~ 路线对应 matchWord,因此尝试第二条路线,所以认为 where 应该换成 .。要解决这个问题,首先要 承认这个判断是对的,因为这是一种 错误提前的情况,只是人类理解时往往只能看到最后几步,所以我们默认用户想要的错误信息,是 正确匹配链路最长的那条,并对 onMatchNode 作出下面优化:将 lastMatch 对象改为 lastMatchUnderShortestRestToken:if ( !lastMatchUnderShortestRestToken || (lastMatchUnderShortestRestToken && lastMatchUnderShortestRestToken.restTokenCount > restTokenCount)) { lastMatchUnderShortestRestToken = { matchNode, token: matchResult.token, restTokenCount };}也就是每次匹配到正确字符,都获取剩余 Token 数量,只保留最后一匹配正确 且剩余 Token 最少的那个。3 总结做语法解析器错误提示功能时,再次刷新了笔者三观,原来我们以为的必然,在编译器里对应着那么多 “可能”。当我们遇到一个错误 SQL 时,错误原因往往不止一个,你可以随便截取一段,说是从这一步开始就错了。语法解析器为了让报错符合人们的第一直觉,对错误信息做了 过滤,只保留剩余 Token 数最短的那条错误信息。4 更多讨论讨论地址是:精读《手写 SQL 编译器 - 错误提示》 · Issue #101 · dt-fe/weekly如果你想参与讨论,请点击这里,每周都有新的主题,周末或周一发布。 ...

September 3, 2018 · 2 min · jiezi