first commit

This commit is contained in:
张乾
2024-10-16 10:18:29 +08:00
parent 9f7e624377
commit 4d66554867
131 changed files with 27252 additions and 0 deletions

View File

@ -0,0 +1,264 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
38 综合实现(一):如何实现面向对象编程?
你好,我是宫文学。
从20世纪90年代起面向对象编程的范式逐渐成为了主流。目前流行度比较高的几种语言比如Java、JavaScript、Go、C++和Python等都支持面向对象编程。
那么,为了支持面向对象编程,我们需要在语言的设计上,以及编译器和运行时的实现上,考虑到哪些问题呢?
这一讲,我就带你来探讨一下如何在一门语言里支持面向对象特性。这是一个很综合的话题,会涉及很多的知识点,所以很有助于帮你梳理和贯通与编译原理有关的知识。
那么,我们就先来分析一下,面向对象特性都包括哪些内容。
面向对象语言的特性
日常中,虽然我们经常会使用面向对象的语言,但如果要问,到底什么才是面向对象?我们通常会说得含含糊糊。最常见的情况,就是会拿自己所熟悉的某种语言的面向对象特性,想当然地认为这就是面向对象语言的全部特性。
不过,在我们的课程里,我想从计算机语言设计的角度,带你重新梳理和认识一下面向对象的编程语言,把面向对象按照清晰的逻辑解构,这样也便于讨论它的实现策略。在这个过程中,你可能会对面向对象产生新的认识。
特征1对象
面向对象编程语言的核心,是把世界看成了一个个的对象,比如汽车、动物等。这些对象包含了数据和代码。数据被叫做字段或属性,而代码通常又被叫做是方法。
此外,这些对象之间还会有一定的关系。比如,汽车是由轮子、发动机等构成的,这叫做聚合关系。而某个班级会有一个班主任,那么班级和作为班主任的老师之间,会有一种引用关系。
对象之间还可以互相发送消息。比如司机会“通知”汽车让它加速或者减速。在面向对象的语言中这通常是通过方法调用来实现的。但也并不局限于这种方式比如对象之间还可以通过异步的消息进行互相通讯不过一般的编程语言都没有原生支持这种通讯方式。我们在讨论Actor模式的时候曾经提到过Actor之间互相通讯的方式就有点像对象之间互发消息。
特征2类和类型体系
很多面向对象的语言都是基于类class并且类也是一种自定义的类型。这个类型是对象的模板。而对象呢则是类的实例。我们还可以再印证一下前面在探究元编程的实现机制时学过的Meta层次的概念。对象属于M0层而类属于M1层它为对象制定了一个标准也就是对象中都包含了什么数据和方法。
其实,面向对象的语言并不一定需要类这个概念,这个概念更多是来自于类型理论,而非面向对象的语言一样可以支持类型和子类型。类型的好处主要是针对静态编译的语言的,因为这样就可以通过类型,来限制可以访问的对象属性和方法,从而减少程序的错误。
而有些面向对象的语言比如JavaScript并没有类的概念。也有的像Python虽然有类的概念但你可以随时修改对象的属性和方法。
特征3重用继承Inheritance和组合Composition
在软件工程里我们总是希望能重用已有的功能。像Java、C++这样的语言能够让子类型重用父类型的一些数据和逻辑这叫做继承。比如Animal有speak()方法Cat是Animal的子类那么Cat就可以继承这个speak()方法。Cat也可以重新写一个方法把父类的方法覆盖掉让叫声更像猫叫。
不过并不是所有的面向对象编程语言都喜欢通过继承的方式来实现重用。你在网上可以找到很多文章都在分析继承模式的缺陷。像Go语言采用的是组合方式来实现重用。在这里我引用了一篇文章中的例子。在这个例子中作者首先定义了一个author的结构体并给这个结构体定义了一些方法
type author struct { //结构体author(作者)
firstName string //作者的名称
lastName string
bio string //作者简介
}
func (a author) fullName() string { //author的方法获取全名
return fmt.Sprintf("%s %s", a.firstName, a.lastName)
}
type post struct { //结构体:文章
title string //文章标题
content string //文章内容
author //文章作者
}
func (p post) details() { //文章的方法:获取文章的详细内容。
fmt.Println("Title: ", p.title)
fmt.Println("Content: ", p.content)
fmt.Println("Author: ", p.author.fullName())
fmt.Println("Bio: ", p.author.bio)
}
关于struct这里我想再给你强调几个知识点。熟悉C语言的同学应该都了解结构体struct。有一些比较新的语言比如Go、Julia和Rust也喜欢用结构体来作为复合数据类型而不愿意用class这个关键字而且它们也普遍摒弃了用继承来实现重用的思路。Go提倡的是组合而我们上一讲提到的泛型也开始在重用方面承担了越来越重要的角色就像在Julia语言里那样Rust和另外一些语言如Scala则使用了一种叫做Trait特征的技术。
Trait有点像Java和Go语言中的接口它也是一种类型。不过它比接口还多了一些功能那就是Trait里面的方法可以具体地实现。
我们用Trait可以替代传统的继承结构比如一个Cat可以实现一个Speakable的Trait而不需要从Animal那里继承。如果Animal还有其他的特征比如说reproduce()繁殖那也可以用一个Trait来代替。这样Cat就可以实现多个Trait。这会让类型体系更加灵活比如实现Speakable的不仅仅有动物还可以是机器人。
在像Scala这样的语言中Trait里不仅仅可以有方法还可以有成员变量而在Ruby语言中我们把这种带有变量和方法的可重用的单元叫做Mixin混入
无论Trait还是Mixin都是基于组合的原理来实现重用的。而且由于继承、接口、Trait和Mixin都可以看做是实现子类型的方式因此也都可以支持多态。因为继承、接口、Trait和Mixin一般都有多个具体的实现类所以在调用相同的方法时会有不同的功能。
特征4封装Encapsulation
我们知道软件工程中的一个原则是信息隐藏我们通常会称为封装encapsulation意思是软件只把外界需要知道的信息和功能暴露出来而内部具体的实现机制只有作者才可以修改并且不会影响到它的使用者。
同样的实现信息封装其实也不是面向对象才有的概念。有的语言的模块Module和包Package都可以作为封装的单元。
在面向对象的语言里通常对象的一些方法和属性可以被公共访问的而另一些方法和属性是内部使用的其访问是受限的。比如Java语言会对可以公共访问的成员加public关键字对只有内部可以访问的成员加private关键字。
好了,以上就是我们总结的面向对象语言的特征了。这里你要注意,面向对象编程其实是一个比较宽泛的概念。对象的概念是它的基础,然后语言的设计者再把类型体系、软件重用机制和信息封装机制给体现出来。在这个过程中,不同的设计者会有不同的取舍。所以,希望你不要僵化地理解面向对象的概念。比如,以为面向对象就必须有类,就必须有继承;以为面向对象才导致了多态,等等。这些都是错误的理解。
接下来,我们再来看看各门语言在实现这些面向对象的特征时,都要解决哪些关键技术问题。
如何实现面向对象的特性?
要实现一门面向对象的语言,我们重点要了解三个方面的关键工作:一是编译器在语法和语义处理方面要做哪些工作;二是运行期对象的内存布局的设计;三是在存在多态的情况下,如何实现方法的绑定。
我们首先从编译器在语法和语义处理上所做的工作开始学起。
编译器前端的工作
我们都知道,编译器的前端必须完成与类和对象有关的语法解析、符号表处理、引用消解、类型分析等工作。那么要实现一门面向对象的语言,编译器也需要完成这些工作。
第一,从语法角度来看,语言的设计者要设计与类的声明和使用有关的语法。
比如:
如何声明一个类?毕竟每种语言的风格都不同。
如何声明类的构造方法?
如何声明类与父类、接口、Trait等的关系
如何实例化一个对象像Java语言就需要new关键字而Python就不需要。
……
也就是说,编译器在语法分析阶段,至少要能够完成上述的语法分析工作。
第二,是要维护符号表,并进行引用消解。
在语义分析阶段,每个类会作为自定义类型被加入到符号表里。这样,在其他引用到该类型的地方,比如用该类型声明了一个变量,或者一个方法的参数或返回值里用到了该类型,编译器就能够做正确的引用消解。
另外,面向对象程序的引用消解还有一个特殊之处。因为父类中的成员变量、方法甚至类型的作用域会延伸到子类,所以编译器要能够在正确的作用域内做引用消解。比如,在一个方法体内,如果发现某个变量既不是本地变量,又不是参数,那么程序就要去找类的成员变量。在当前的类里找不到,那就要到父类中逐级去找。
还有一点,编译器在做引用消解的时候,还可以完成访问权限的检查。我们知道,对象要能够实现信息封装。对于编译器来说,这个功能实现起来很简单。在做引用消解的时候,检查类成员的访问权限就可以了。举个例子,假设你用代码访问了某个私有的成员变量,或者私有的方法,此时程序就可以报错;而在这个类内部的代码中,就可以访问这些私有成员。这样就实现了封装的机制。
第三,要做类型检查和推断。
使用类型系统的信息,在变量赋值、函数调用等地方,会进行类型检查和推断。我们之前学过的关于子类型、泛型等知识,在这里都可以用上。
OK以上就是编译器前端关于实现面向对象特性的重点工作了我们接下来看看编译器在运行时的一个设计重点就是对象的内存布局。
对象的内存布局
在第二个模块研究几个不同的编译器的时候我们已经考察过各种编译器在保存对象时所采用的内存布局。像Java、Python和Julia的对象一般都有一个固定的对象头。对象头里可以保存各种信息比如到类定义的指针、与锁有关的标志位、与垃圾收集有关的标志位等等。
对象头之后通常就是该类的数据成员。如果存在父类那么就既要保存父类中的成员变量也要保存子类中的成员变量。像Python这样的语言对内存的使用比较浪费通常是用一个内部字典来保存成员变量但对于Java这样的语言则要尽量节约着用内存。
我举个例子。假设某个Java类里有两个成员变量那么这两个成员变量会根据声明的顺序排在对象头的后面。如果成员变量是像Int这样的基础数据那么程序就要保存它的值而如果是String等对象类型那么就要保存一个指向该对象的指针。
在Java语言中当某个类存在父类的情况下那么父类的数据成员一定要排在前面。
这是为什么呢我给你举一个例子。在这个例子中有一个父类Base有两个子类分别是DerivedA和DerivedB。
图1Base类有两个子类
如果两个子类分别有一个实例a和b那么它们的内存布局就是下面的样子
图2两个子类的实例的内存布局
那么你可能要问了,为什么父类的数据成员要放在前面,子类的要放在后面?这也要从编译的角度说起。
我们知道,在生成的汇编代码里,如果要访问一个类的成员变量,其实都是从对象地址加上一个偏移量,来得到成员变量的地址。而这样的代码,针对父类和各种不同的子类的对象,要都能正常运行才行。所以,该成员变量在不同子类的对象中的位置,最好是固定的,这样才便于生成代码。
不过像C++这样的语言由于它经常被用来编写系统级的程序所以它不愿意浪费任意一点内存因此就不存在对象头这样的开销。但是由于C++支持多重继承所以当某个类存在多个父类的情况下在内存里安排不同父类的成员变量以及生成访问它们的正确代码就要比Java复杂一些。
比如下面的示例代码中c同时继承了a和b。你可以把对象obj的地址分别转换为a、b和c的指针并把这个地址打印出来。
class a { int a_; };
class b { int b_; };
class c : public a, public b { };
int main(){
c obj;
printf("a=0x%08x, b=0x%08x, c=0x%08x\n", (a*)&obj,(b*)&obj,(c*)&obj);
}
看到这段代码,你发现什么了呢?
你会发现a和c的指针地址是一样的而b的地址则要大几个字节。这是因为在内存里程序会先排a的字段再排b的字段最后再排c的字段。编译器做指针的类型转换cast的时候要能够计算出指针的正确地址。
图3多重继承情况下的内存布局
好,现在你就已经见识到了编译器在运行期针对对象的内存布局的设计了。接下来,我们再来看看针对多态的情况,编译器对实现方法的绑定是怎么做的。
方法的静态绑定和动态绑定
当你的程序调用一个对象的方法的时候这个方法到底对应着哪个实现有的时候在编译期就能确定了比如说当这个方法带有private、static或final关键字的时候。这样你在编译期就知道去执行哪段字节码这被叫做静态绑定Static Binding也可以叫做早期绑定Early Binding或者静态分派Static Dispathing
另外对于重载Overload的情况也就是方法名称一样、参数个数或类型等不一样的情况也是可以在编译期就识别出来的所以也可以通过静态绑定。
而在存在子类型的情况下我们到底执行哪段字节码只有在运行时根据对象的实际类型才能确定下来。这个时候就叫做动态绑定Dynamic binding也可以叫做后期绑定Late binding或者动态分派Dynamic Dispatching
动态绑定也是面向对象之父阿伦 · 凯伊Alan Kay所提倡的面向对象的特征绑定时机要尽量地晚。绑定时机晚意味着你在编程的时候可以编写尽量通用的代码也就是代码里使用的是类型树中更靠近树根的类型这种类型更通用也就可以让使用这种类型编写的代码能适用于更多的子类型。
那么动态绑定在运行时是怎么实现的呢对于Java语言来说其实现机制对于每个JVM可以是不同的。不过我们可以参考C++的实现机制就可以猜测出JVM的实现机制。
在C++语言中动态绑定是通过一个vtable的数据结构来实现的。vtable是Virtual Method Table或Virtual Table的简称。在C++里如果你想让基类中的某个方法可以被子类覆盖Override那么你在声明该方法的时候就要带上virtual关键字。带有虚方法的类及其子类的对象实例都带有一个指针指向一个表格这也就是vtable。
vtable里保存的是什么呢是每个虚方法的入口地址。我们来看一个例子这个例子中有Base、DerivedA和DerivedB三个类
图4vtable和动态绑定的原理
在对象a和b的vtable中各个方法的指针都会指向这个对象实际调用的代码的入口地址。
那么编译器的工作就简单了。因为它可以基于对象的地址得到vtable的地址不用去管这个对象是哪个子类的实例。
然后编译器只需要知道当前调用的方法在vtable中的索引值就行了这个索引值对于不同的子类的实例也都是一样的但是具体指向的代码地址可能会不同。这种调用当然需要比静态绑定多用几条指令因为对于静态绑定而言只需要跳转到某个特定的方法的代码地址就行了不需要通过vtable这个中间数据结构。不过这种代价是值得的因为它支持了面向对象中基于子类型的多态从而可以让你编写更具通用性的代码。
图4中我展示的只是一个示意结构。实际上vtable中还包含了一些其他信息比如能够在运行时确定对象类型的信息这些信息也可以用于在运行时进行指针的强制转换。
上面的例子是单一继承的情况。对于多重继承还会有多个vptr指针指向多个vtable。你参考一下多重继承下的内存布局和指针转换的情况应该就可以自行脑补出多重继承下的方法绑定技术。
我们接着回到Java。Java语言中调用这种可被重载的方法生成的是invokevirtual指令你在之前阅读Java字节码的时候一定遇到过这个指令。那么我现在可以告诉你这个virtual就是沿用了C++中虚方法的概念。
OK理解了实现面向对象特性所需要做的一系列重点工作以后我们来挑战一个难度更高的目标这也是一个有技术洁癖的人会非常想实现的目标就是让一切数据都表达为对象。
如何实现一切数据都是对象?
在Java、C++和Go这些语言中基础数据类型和对象类型是分开的。基础数据类型包括整型、浮点型等它们不像对象类型那样有自己的方法也没有类之间的继承关系。
这就给我们的编程工作造成了很多不便。比如针对以上这两类不同数据类型的编程方式是不一致的。在Java里你不能声明一个保存int数据的ArrayList。在这种情况下你只能使用Integer类型。
不过Java语言的编译器还是尽量提供了一些便利。举个例子下面示例的两个语句都是合法的。在需要用到一个Interger对象的时候你可以使用一个基础的int型数据反过来亦然。
Integer b = 2;
int c = b + 1;
在研究Java编译器的时候你已经发现它在语义分析阶段提供了自动装箱boxing和拆箱unboxing的功能。比如说如果发现代码里需要的是一个Interger对象而代码里提供的是一个int数据那么程序就自动添加相应的AST节点基于该int数据创建一个Integer对象这就叫做装箱功能。反之呢把Integer对象转换成一个int数据就叫做拆箱功能。装箱和拆箱功能属于一种语法糖它能让编程更方便一些。
说到这里你可能会想到既然编译器可以实现自动装箱和拆箱那么在Java语言里是不是根本就不用提供基础数据类型了全部数据都用对象表达行不行这样的话语言的表达性会更好我们写起程序来也更简单。
不过现在要想从头修改Java的语法是不可能了。但也有其他基于JVM的语言做到了这一点比如Scala。在Scala里所有数据都是对象各个类型构成了一棵有着相同根节点的类型树。对于对象化的整型和浮点型数据编译器可以把它们直接编译成JVM的基础数据类型。
可仅仅这样还不够。在Java里面需要自动装箱和拆箱机制很大一部分原因是Java的泛型机制。那些使用泛型的List、Map等集合类只能保存对象类型不能保存基础数据类型。但这对于非常大的一个集合来说用对象保存整型数据要多消耗几倍的内存。那么我们能否优化集合类的实现让它们直接保存基础数据而不是保存一个个整型对象的引用呢
通过上一讲的学习我们也知道了Java的泛型机制是通过类型擦除来实现的所以集合类里面只能保存对象引用无法保存基础数据。既然JVM平台缺省的类型擦除技术行不通那么是否可以对类型参数是值类型的情况下做特殊处理呢
这是可以做到的。你还记得C++实现泛型采用的是元编程技术。那么在JVM平台上你其实也可以通过元编程技术针对值类型生成不同的代码从而避免创建大量的小对象降低内存占用同时减少GC的开销。Scala就是这么做的它会通过注解技术来完成这项任务。如果你对Scala的具体实现机制感兴趣可以参考这篇文章。
课程小结
这一讲我通过面向对象这个话题,带你一起综合性地探讨了语言设计、编译器和运行时的多个知识点。你可以带走这几个关键知识点:
第一,要正确地理解面向对象编程的内涵,知道其实面向对象的语言可以有多种不同的设计选择,体现在类型体系、重用机制和信息封装等多个方面。对于不同的设计选择,你要都能够把它们解构,并对应到编译期和运行时的设计上。
第二,面向对象的各种特性,大多都是要在语义分析阶段进行检查和验证。
第三,对于静态编译的面向对象语言来说,理解其内存布局是关键。编译期要保证能够正确地访问对象的属性,并且巧妙地实现方法的动态绑定。
第四,如有可能,尽量让一切数据都表达为对象。让编译器完成自动装箱和拆箱的工作。
按照惯例,我把这节课的核心内容整理成了思维导图,供你参考和回顾知识点。
一课一思
有人曾经在技术活动上问Java语言之父詹姆斯 · 高斯林James Gosling如果重新设计Java语言他会怎么做他回答说他会去掉class也就是会取消类的继承机制。那么对于你熟悉的面向对象语言如果有机会重新设计的话你会怎么建议为什么欢迎分享你的观点。
参考资料
介绍Trait机制的论文。
在类型参数是值类型的情况下Scala以特殊的方式做实例化的文章。

View File

@ -0,0 +1,373 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
39 综合实现(二):如何实现函数式编程?
你好,我是宫文学。
近些年函数式编程正在复兴。除了一些纯函数式编程语言比如Lisp、Clojure、Erlang等众多的主流编程语言如Python、JavaScript、Go甚至Java它们都有对函数式编程的支持。
你应该会发现,现在人们对于函数式编程的讨论有很多,比如争论函数式编程和面向对象编程到底哪个更强,在语言里提供混合的编程模式到底对不对等等。
这些论战一时半会儿很难停息。不过我们的这一讲,不会涉及这些有争议的话题,而是试图从编译技术的角度,来探讨如何支持函数式编程,包括如何让函数作为一等公民、如何针对函数式编程的特点做优化、如何处理不变性,等等。通过函数式编程这个综合的主题,我们也再一次看看,如何在实现一门语言时综合运用编译原理的各种知识点,同时在这个探究的过程中,也会加深你对函数式编程语言的理解。
好,我们先来简单了解一下函数式编程的特点。
函数式编程的特点
我想,你心里可能多多少少都会有一点疑问,为什么函数式编程开始变得流行了呢?为什么我在开篇的时候,说函数式编程正在“复兴”,而没有说正在兴起?为什么围绕函数式编程会有那么多的争论?
要回答这几个问题,我会建议你先去了解一点历史。
我们都知道,计算机发展历史上有一个重要的人物是阿兰 · 图灵Alan Turing。他在1936年提出了一种叫做图灵机的抽象模型用来表达所有的计算。图灵机有一个无限长的纸带还有一个读写头能够读写数据并根据规则左右移动。这种计算过程跟我们在现代的计算机中用一条条指令驱动计算机运行的方式很相似。
不过,计算模型其实不仅仅可以用图灵机来表达。早在图灵机出现之前,阿隆佐 · 邱奇Alonzo Church就提出了一套Lambda演算的模型。并且计算机科学领域中的很多人其实都认为用Lambda演算来分析可计算性、计算复杂性以及用来编程会比采用图灵机模型更加简洁。而Lambda演算就是函数式编程的数学基础。
补充:实际上,邱奇是图灵的导师。当年图灵发表他的论文的时候,编辑看不懂,所以找邱奇帮忙,并推荐图灵成为他的学生,图灵机这个词也是邱奇起的。所以师生二人,对计算机科学的发展都做出了很大的贡献。
因为有Lambda演算的数学背景所以函数式编程范式的历史很早。上世纪50年代出现的Lisp语言就是函数式编程语言。Lisp的发明人约翰 · 麦卡锡John McCarthy博士是一位数学博士。所以你用Lisp语言和其他函数式编程语言的时候都会感觉到有一种数学思维的味道。
也正因如此与函数式编程有关的理论和术语其实是有点抽象的比如函子Functor、单子Monad、柯里化Currying等。当然对它们的深入研究不是我们这门课的任务。这里我想带你先绕过这些理论和术语从我们日常的编程经验出发来回顾一下函数式编程的特点反倒更容易一些。
我前面也说过目前流行的很多语言虽然不是纯粹的函数式编程语言但多多少少都提供了对函数式编程的一些支持比如JavaScript、Python和Go等。就连Java语言也在Java8中加入了对函数式编程的支持很多同学可能已经尝试过了。
我们使用函数式编程最多的场景恐怕是对集合的处理了。举个例子假设你有一个JavaScript的数组a你想基于这个数组计算出另一个数组b其中b的每个元素是a中对应元素的平方。如果用普通的方式写程序你可能会用一个循环语句遍历数组a然后针对每个数组元素做处理
var b = [];
for (var i = 0; i< a.length; i++){ //遍历数组a
b.push(a[i]*a[i]); //把计算结果加到数组b中
}
不过你也可以采用更简单的实现方法
这次我们使用了map方法并给它传了一个回调函数map方法会针对数组的每个元素执行这个回调函数并把计算结果组合成一个新的数组
function sq(item){ //计算平方值的函数
return item*item;
}
var b = a.map(sq); //把函数作为参数传递
它还可以写成一种更简化的方式也就是Lambda表达式的格式
var b = a.map(item=>item*item);
通过这个简单的例子,我们可以体会出函数式编程的几个特点:
1.函数作为一等公民
也就是说函数可以像一个数值一样被赋给变量也可以作为函数参数。如果一个函数能够接受其他函数作为参数或者能够把一个函数作为返回值那么它就是高阶函数。像示例程序中的map就是高阶函数。
那函数式编程语言的优势来自于哪里呢?就在于它可以像数学那样使用函数和变量,这会让软件的结构变得特别简单、清晰,运行结果可预测,不容易出错。
根据这个特点,我们先来看看函数式编程语言中的函数,跟其他编程语言中的函数有什么不同。
2.纯函数Pure Function
在函数式编程里面,有一个概念叫做纯函数。纯函数是这样一种函数,即相同的输入,永远会得到相同的输出。
其实你对纯函数应该并不陌生。你在中学时学到的函数就是纯函数。比如对于f(x)=ax+b对于同样的x所得到的函数值肯定是一样的。所以说纯函数不应该算是个新概念而是可以回归到你在学习计算机语言之前的那个旧概念。
在C语言、Java等语言当中由于函数或方法里面可以引用外面的变量比如全局变量、对象的成员变量使得其返回值与这些变量有关。因此如果有其他软件模块修改了这些变量的值那么该函数或方法的返回值也会受到影响。这就会让多个模块之间基于共享的变量耦合在一起这种耦合也使得软件模块的依赖关系变得复杂、隐秘容易出错牵一发而动全身。这也是像面向对象语言这些命令式编程语言最令人诟病的一点。
而对于纯函数来说它不依赖外部的变量这个叫做引用透明Reference Transparency。纯函数的这种“靠谱”、可预测的特征就给我们的编程工作带来了很多的好处。
举个例子。既然函数的值只依赖输入那么就跟调用时间无关了。假设有一个函数式g(f(x))如果按照传统的求值习惯我们应该先把f(x)的值求出来再传递给g()。但如果f(x)是纯函数那么早求值和晚求值其实是无所谓的所以我们可以延迟求值Lazy Evaluation
延迟求值有很大的好处。比如在下面的伪代码中unless是一个函数f(x)是传给它的一个参数。在函数式编程语言中只有当condition为真时才去实际对f(x)求值。这实际上就降低了工作量。
//在满足条件时执行f(x)
unless(condition, f(x));
//伪代码
int unless(bool condition, f(x)){
if (condition)
return f(x);
}
再回到纯函数。我说纯函数的输出仅依赖输入有一点需要说明就是函数只有返回值这一种输出没有其他的输出。换句话说纯函数没有副作用Side Effect
什么是副作用呢?简单地说,就是函数在运行过程中影响了外界环境。比如,修改了一个全局变量或者是对象的属性、往文件里写入内容、往屏幕上打印一行字、往数据库插入一条记录、做了一次网络请求,等等。也就是说,纯函数要求程序除了计算,其他的事情都不要做。
如果函数有副作用的话那么我们前面说的时间无关性就被破坏了。比如说原来a函数是在屏幕上打印“欢迎b函数是屏幕输出你的名字最后形成“欢迎XXX”。那么a和b的前后顺序就不能颠倒。
你可能会说,一个有用的程序,哪能没有副作用呀。你说得对。在函数式编程里,程序会尽量把产生副作用的函数放在调用的外层,而完成内部功能的大部分函数,都保持是纯函数。比如,最外层的函数接受网络请求,并对客户端返回结果,它是有副作用的。而程序所使用的其他函数,都没有副作用。
纯函数的功能是如此地简单纯粹以至于它还能继续带来一些好处。比如说像Erlang这样的语言可以在运行时给某些函数升级而不用重启整个系统。为什么呢因为这些升级后的函数针对相同的输入程序得到的结果是一样的那么对这个函数的使用者来说就没有任何影响。这也是用Erlang写的系统会具有很高的可靠性的原因之一。
不过函数式编程语言里使用的也不全都是纯函数比如有的函数要做一些IO操作。另外闭包是函数引用了词法作用域中的自由变量而引起的所以也不是纯函数。
总结起来,在函数式编程中,会希望函数像数学中的函数那样纯粹,即不依赖外部(引用透明),也不改变外部(无副作用),从而带来计算时间、运行时替换等灵活性的优势。
好,说完了函数的不同,我们再来看看函数式编程语言里使用变量跟其他语言的不同。
3.不变性Immutability
我们都知道在数学里面当我们用到x和y这样的变量的时候它所代表的值在计算过程中是不变的。
没错,这也是函数式编程的一个重要原则,不变性。它的意思是,程序会根据需要来创建对象并使用它们,但不会去修改对象的状态。如果有需要修改对象状态的情况,那么去创建一个新对象就好了。
在前面的示例程序中map函数返回了一个新的数组而原来的数组保持不变。这就体现了不变性的特点。
不变性也会带来巨大的好处。比如说,由于函数不会修改对象的状态,所以就不存在并发程序中的竞争情况,进而也就不需要采用锁的机制。所以说,函数式编程更适合编写并发程序。这个优势,也是导致这几年函数式编程复兴的重要原因。
好,那么最后,我们再来注意一下函数式编程语言在编程风格上的不同。
4.声明式Declarative的编程风格
在计算机语言中,实现编程的方式主要有几种。
第一种实现方式我们会一步步告诉计算机该去怎么做计算循环访问a的元素计算元素的平方值并加到b中。这种编程风格叫做命令式Imperative编程即命令计算机按照你要求的步骤去做。命令式编程风格植根于现代计算机的结构因为机器指令本质上就是命令式的。这也是图灵机模型的特点。
而第二种实现方式叫做声明式Declarative编程。这种编程风格会要求计算机给出你想要的结果而不关心过程。比如在前面的示例程序中你关心的是对数组中的每个元素计算出平方值。至于具体的处理步骤是对数组a的元素顺序计算还是倒序计算你并不关心。
声明式编程风格的另一个体现,是递归函数的大量使用。这是因为我们描述一个计算逻辑的时候,用递归的方式表达通常会更简洁。
举个例子。你可能知道斐波纳契Fibonacci数列中的每个数是前两个数字的和。这个表达方式就是递归式的。写成公式就是Fibonacci(n)=Fibonacci(n-1)+Fibonacci(n-2)。这个公式与我们用自然语言的表达完全同构,也更容易理解。
我把计算斐波纳契数列的程序用Erlang这种函数式语言来写一下你可以进一步体会到声明式编程的那种简洁和直观的特点
%% 计算斐波那契的第N个元素
fibo(1) -> 1; %%第一个元素是1
fibo(2) -> 1; %%第二个元素也是1
fibo(N) -> fibo(N-1) + fibo(N-2). %%递归
好了,现在我们已经了解了函数式编程的一些关键特征。它的总体思想呢,就是像数学那样去使用函数和值,使可变动部分最小化,让软件的结构变得简单、可预测,从而获得支持并发、更简洁的表达等优势。那么下面,我们就一起来看看如何结合编译原理的相关知识点,来实现函数式编程的这些特征。
函数式编程语言的编译和实现
为了实现函数式语言,我们在编译期和运行时都要做很多工作。比如,要在编译器前端做分析和各种语义的检查; 要以合适的方式在程序内部表示一个函数;要针对函数式编程的特点做特别的优化,等等。接下来我们就从编译器的前端工作开始学起。
编译器前端的工作
函数式编程语言,在编译器的前端也一样要做很多的语法分析和语义分析工作。
你应该知道语言的设计者需要设计出如何声明一个函数。像是JavaScript语言会用function关键字Go语言用func关键字Rust语言用的是fn关键字而C语言根本不需要一个关键字来标识一个函数的定义另外如何声明函数的参数和返回值也会使用不同的语法。编译器都要能够正确地识别出来。
语义分析的工作则更多,包括:
符号表和引用消解:当声明一个函数时,要把它加入到符号表。而当程序中用到某个函数的时候,要找到该函数的声明。
类型检查和推导既然函数可以被当做一个值使用那么它一定也是有类型的也要进行类型检查和推导。比如在程序的某个地方只能接受返回值为int有一个参数为String的函数那么就需要被使用的函数是否满足这个要求。关于函数的类型一会儿我还会展开讲解。
语法糖处理在函数式编程中经常会使用一些语法糖。最常见的语法糖就是Lambda表达式Lambda表达式可以简化匿名函数的书写。比如前面JavaScript的示例代码中对数组元素求平方的函数可以写成一个Lambda表达式从而把原来的代码简化成了一行
var d = a.map(item=>item*item); //括号中是一个lambda表达式
在这个示例程序中,=>左边的是匿名函数的参数右边的是一个表达式这个表达式的计算结果就是匿名函数的返回值。你看通过一个Lambda表达式代替了传统的函数声明代码也变得更简洁了。
OK因为在编译器前端还要对函数做类型分析所以我们再来探究一下函数的类型是怎么一回事。
把函数纳入类型系统
这里我要先提一个问题,就是在函数式编程语言里,既然它能够把函数当做一个值一样去看待,那么也应该有相应的类型吧?这就要求语言的类型系统能够把函数包含进来。因此函数式编程语言在编译的时候,也要进行类型检查和类型推断。
不过,我们在谈论类型时,比较熟悉的是值类型(如整型、浮点型、字符型),以及用户自定义的类型(如结构、类这些),如果函数也是一种类型,那跟它们是什么关系呢?如果由你来设计,那么你会怎么设计这个类型体系呢?
在不同的语言里设计者们是以不同的方式来解决这个问题的。拿Python来说Python中一切都是对象函数也不例外。函数对象的ob_type字段也被设置了合适的类型对象。这里你可以再次回味一下Python的类型系统设计得是如何精巧。
我们再看看Scala的类型系统。上一讲我提出过Scala实现了一个很漂亮的类型系统把值类型和引用类型也就是自定义类做了统一。它们都有一个共同的根就是Any。由于Scala是基于JVM的所以这些类型最后都是以Java的类来实现的。
那么函数也不例外。因为Scala的函数最多支持22个参数所以Scala里有内置的Function1、Function2…Function22这些类作为函数的类型它们也都是Any的子类型。每个Scala函数实际上是这些类的实例。
另外Swift语言的文档对类型的定义也比较清楚。它以产生式的方式列出了type的语法定义。根据该语法类型可以是函数类型、数组类型、字典类型、元组类型等等这些都是类型。
并且它还把所有类型分成了两个大类别命名类型Named Type和复合类型Compound Type
命名类型包括类、结构体、枚举等,它们都有一个名称,比如自定义类的类名就是类型名称。
复合类型则没有名称,它是由多个其他类型组合而成的。函数和元组都属于复合类型。函数的类型是由参数的类型和返回值的类型组合而成的,它们都是编译器对函数类型进行计算的依据。
举例来说假设一个函数有两个参数分别是类型A和B而返回值的类型是C那么这个函数的类型可以计为(A, B)->C。这就是对函数的类型的形式化的表达。
那么进一步,我们如何在编译期里针对函数的类型做类型分析呢?它跟非复合的类型还真不太一样,因为编译器需要检查复合类型中的多个元素。
举个例子。在一个高阶函数g()里能够接收一个函数类型的参数f(A,B),要求其类型是(A, B)->C而实际提供的函数f2的类型是(A1, B1)->C1那么你在编译器里如何判断函数的类型是否合法呢这里的算法要做多步的检查
第一f2也必须有两个参数这点是符合的。
第二检查参数的类型。A1和B1必须是跟A和B相同的类型或者是它们的父类型这样f1才能正确地给f2传递参数。
第三检查返回值的类型。C1则必须是C的子类型这样f1才能接收f2的返回值。
好,说完了编译器的前端工作,我们再来看看函数在语言内部的实现。
函数的内部实现
在函数式编程里,所有一切都围绕着函数。但是在编译完毕以后,函数在运行时中是怎么表示的呢?
就像不同的面向对象的语言,在运行时是以不同的方式表示一个对象的,不同的函数式编程语言,在运行时中去实现一个函数的机制也是不太一样的。
在Python中一切都是对象所以函数也是一种对象它是实现了Callable协议的对象能够在后面加上一对括号去调用它。
在Scala和Java这种基于JVM的语言中函数在JVM这个层次没有获得原生支持因此函数被编译完毕以后其实会变成JVM中的类。
在Julia、Swift、Go、Rust这样编译成机器码的语言中函数基本上就是内存中代码段或文本段的一个地址。这个地址在编译后做链接的时候会变成一个确定的地址值。在运行时跳转到这个地址就可以执行函数的功能。
补充:再具体一点的话,编译成机器码的函数有什么特点呢?我们再来回顾一下。
首先,函数的调用者要根据调用约定,通过栈或者寄存器设置函数的参数,保护好自己负责保护的寄存器以及返回地址,然后调用函数。
在被调用者的函数体内通常会分为三个部分。头尾两个部分叫做序曲prelude和尾声epilogue分别做一些初始化工作和收尾工作。在序曲里会保存原来的栈指针以及把自己应该保护的寄存器存到栈里、设置新的栈指针等接着执行函数的主体逻辑。最后到尾声部分要根据调用约定把返回值设置到寄存器或栈恢复所保护的寄存器的值和栈顶指针接着跳转到返回地址。
返回到调用者以后,会有一些代码恢复被保护起来的寄存器,获取返回值,然后继续执行后面的代码。
这样,把上述整个过程的细节弄清楚了,你就知道如何为函数生成代码了。
最后,我们必须提到一种特殊情况,就是闭包。闭包是纯函数的对立面,它引用了上级作用域中的一些自由变量。闭包在运行时不仅是代码段中的一个函数地址,还必须保存自由变量的值。为了实现闭包的运行时功能,编译器需要生成相应的代码,以便在生成闭包的时候,可以在堆里申请内存来保存自由变量的值。而当该闭包不再被引用了,那么就会像不再被引用的对象一样,成为了内存垃圾,要被垃圾回收机制回收。
好了,到这里你可能会觉得,看上去函数的实现似乎跟命令式语言也没有什么不同。不过,接下来你就会看到不同点了,这就是延迟求值的实现。
延迟求值Lazy Evaluation
在命令式语言里我们对表达式求值是严格按照顺序对AST求值。但对于纯函数来说由于在任何时候求值结果都是一样的因此可以进行一定的优化比如延迟求值Lazy Evaluation从而有可能减少计算工作量或者实现像unless()那样的特别的控制结构。
那么针对这种情况,编译器需要做什么处理呢?
我举个例子,对于下面的示例程序(伪代码):
g(condition, x){
if (condition)
return x;
else return 0;
}
如果我们调用的时候在x参数的位置传入的是另一个函数调用f(y)也就是g(condition, f(y))那么编译器就会把g()的函数体内用到x的地方都转换成对f(y)的调用:
if (condition)
return f(y);
else return 0;
这种把对参数的引用替换成对函数调用的技术,叫做换名调用。
不过换名调用有一个缺点就是f(y)有可能会被多次调用,而且每次调用的结果都是一样的。这就产生了浪费。那么这时,编译器就要更聪明一点。
怎么办呢?那就是在第一次调用的时候,记录下它的值。如果下次再调用,则使用第一次调用的结果。这种方式叫做按需调用。
总而言之,纯函数的特征就导致了延迟求值在编译上的不同。而函数式编程另一个重要的特征,不变性,也会对编译和运行过程造成影响。
不变性对编译和运行时的影响
在遵守不变性原则的情况下,对程序的编译也会有很大的不同。
第一由于函数不会修改对象的状态所以就不存在并发程序中的竞争情况进而也就不需要采用锁的机制编译器也不需要生成与锁有关的代码。Java、JavaScript等语言中关于参数逃逸的分析也变得不必要了因为反正别的线程获得了某个对象或结构体也不会去修改它的状态。
第二,不变性就意味着,只可能是新的对象引用老的对象,老的对象不可能引用新的对象。这对于垃圾收集算法的意义很大。在分代收集的算法中,如果老对象被新对象引用,那必须等到新对象回收之后老对象才可能被回收,所以函数式编程的程序现在可以更容易做出决定,把老对象放到老一代的区域,从而节省垃圾收集算法的计算量;另外,由于对象不会被改变,因此更容易实现增量收集和并行收集;由于不可能存在循环引用,因此如果采用的是引用计数法的话,就没有必要进行循环引用的检测了。
第三,不变性还意味着,在程序运行过程中可能要产生更多的新对象。在命令式语言中,程序需要对原来的对象修改状态。而函数式编程,只能每次创建一个新对象。所以,垃圾收集算法需要能够尽快地收集掉新对象。
OK了解了不变性我们再来看看针对函数式编程语言的优化算法。其中最重要的就是对递归函数的优化。
对递归函数的优化
虽然命令式的编程语言也会用到递归函数但函数式编程里对递归函数的使用更加普遍比如通常会用递归来代替循环。如果要对一个整型数组求和命令式编程语言会做一个循环而函数式编程语言则更习惯于用递归的方式表达sum(a, i) = a[i] + sum(a, i-1)。
按照传统的函数调用的运行方式,对于每一次函数调用,程序都要增加一个栈桢。递归调用一千次,就要增加一千个栈桢。这样的话,程序的栈空间很快就会被耗尽。并且,函数调用的时候,每次都要有一些额外的开销,比如保护寄存器的值、保存返回地址、传递参数等等。
我在第7讲的优化算法里提到过尾调用优化也就是执行完递归函数后马上用return语句返回的情况。
f(x){
....
return g(...); //尾调用
}
在尾调用的场景下,上一级函数的栈桢已经没什么用了,程序可以直接复用。函数调用的过程,可以被优化成指令的跳转,不需要那些函数调用的开销。
不过对于递归调用的情况往往还需要对递归函数返回值做进一步的计算。比如在下面的求阶乘的函数示例中返回值是x*fact(x-1)。
//fact.c 求阶乘
int fact(int x){
if (x == 1)
return 1;
else
return x*fact(x-1); //对递归值要做进一步的计算
}
对于编译器来说它可以经过分析把这种情况转换成一个单纯的尾调用。具体地说就是它相当于引入了一个临时的递归函数fact2()并且用第一个参数acc来记录累计值
int fact(x){
if (x == 1)
return 1;
else
return fact2(x, x-1); //调用一个临时的递归函数
}
int fact2(int acc, int x){ //参数acc用来保存累计值
if (x == 1){
return acc;
}
else{
return fact2(acc * x, x-1); //一个单纯的尾调用
}
}
如果我们调用fact(5)其实际执行过程就会在acc参数中连续地做乘法从而实现阶乘
->fact(5)
->fact2(5,4)
->fact2(5*4,3)
->fact2(5*4*3,2)
->fact2(5*4*3*2,1)
->5*4*3*2
你可以观察一下编译器实际生成的汇编程序看看优化后的成果。如果用“clang -O1 -S -o fact.s fact.c”来编译fact函数就会得到一个汇编代码文件。我对这段代码做了注释你可以理解下它的逻辑。你可以发现优化后的函数没有做任何一次递归调用。
_fact: ## @fact
pushq %rbp # 保存栈底指针
movq %rsp, %rbp # 把原来的栈顶,设置为新栈桢的栈底
movl $1, %eax # %eax是保存返回值的。这里先设置为1
cmpl $1, %edi # %edi是fact函数的第一个参数相当于if(x==1)
je LBB0_3 # 如果相等跳转到LBB0_3就会直接返回1
movl $1, %eax # 设置%eax为1这里%eax会保存累计值
LBB0_2:
imull %edi, %eax # 把参数乘到%eax来
decl %edi # x = x-1
cmpl $1, %edi # x是否等于1
jne LBB0_2 # 如果不等跳到LBB0_2做连乘
LBB0_3:
popq %rbp # 回复原来的栈底指针
retq # 返回
要想完成这种转换就要求编译器能够基于IR分析出其中的递归结构然后进行代码的变换。
课程小结
这一讲,我们一起讨论了实现函数式编程特性的一些要点。我希望你能记住这些关键知识点:
第一函数式编程的理论根基可以追溯到比图灵机更早的Lambda演算。要理解函数式编程的特点你可以回想一下中学时代数学课中的内容。在函数式编程中函数是一等公民。它通过强调纯函数和不变性大大降低了程序的复杂度使软件不容易出错并且能够更好地支持并发编程。并且由于采用声明式的编程风格往往程序可以更简洁表达性更好。
第二,不同的语言实现函数的机制是不同的。对于编译成机器码的语言来说,函数就是一个指向代码的指针。对于闭包,还需要像面向对象的语言那样,管理它在内存中的生存周期。
第三,函数仍然要纳入类型体系中,编译器要支持类型的检查和推断。
第四,针对函数式编程的特点,编译器可以做一些特别的优化,比如延迟求值、消除与锁有关的分析、对递归的优化等等。
同样,我把这一讲的知识点梳理成了思维导图,供你参考:
一课一思
这节课中我提到,在很多情况下,用函数式编程表达一个计算逻辑会更简洁。那么,你能不能找到这样的一些例子?欢迎分享你的经验。
如果你身边也有对函数式编程感兴趣的朋友,那么也非常欢迎你把这节课分享给 TA。感谢你的阅读下一讲我们会一起解析华为的方舟编译器到时候再见

View File

@ -0,0 +1,285 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
40 成果检验:方舟编译器的优势在哪里?
你好,我是宫文学。到这里,咱们的课程就已经进入尾声了。在这门课程里,通过查看真实的编译器,你应该已经积累了不少对编译器的直观认识。前面我们研究的各种编译器,都是国外的产品或项目。而这一讲呢,我们则要看看一个有中国血统的编译器:方舟编译器。
通过阅读方舟编译器已经公开的代码和文档,在解析它的过程中,你可以检验一下自己的所学,谈谈你对它的认识。比如,跟你了解的其他编译器相比,它有什么特点?先进性如何?你是否有兴趣利用方舟编译器做点实际项目?等等。
不过到目前为止由于方舟编译器开源的部分仍然比较有限所以这一讲我们只根据已经掌握的信息做一些分析。其中涉及两个大的话题一是对方舟编译器的定位和设计思路的分析二是对方舟编译器所使用的Maple IR的介绍。
首先我借助Android对应用开发支持的缺陷来谈一下为什么方舟编译器是必要的。
Android的不足
为什么要研发一款自己的编译器?对于一个大的技术生态而言,语言的编译和运行体系非常重要。它处在上层应用和下层硬件之间,直接决定了应用软件能否充分地发挥出硬件的性能。对于移动应用生态而言,我国拥有体量最大的移动用户和领先的移动应用,也有着最大的手机制造量。可是,对于让上层应用和底层硬件得以发挥最大能力的编译器和运行时,我们却缺少话语权。
实际上我认为Android对应用开发的支持并不够好。我猜测掌控Android生态的谷歌公司对于移动应用开发和手机制造都没有关系到切身利益因此创新的动力不足。
我之所以说Android对应用开发的支持不够好这其实跟苹果的系统稍加对比就很清楚了。同样的应用在苹果手机上会运行得更流畅且消耗的内存也更低。所以Android手机只好增加更多的CPU内核和更多的内存。
你可能会问谷歌不是也有自己的应用吗对应用的支持也关系到谷歌自己的利益呀。那我这里其实要补充一下我说的应用开发指的是用Java和Kotlin开发的应用这也是大部分Android平台上的应用开发者所采用的语言。而像谷歌这样拥有强大技术力量的互联网巨头们通常对于性能要求比较高的代码是用C开发的。比如微信的关键逻辑就是用C编写的像手机游戏这种对性能要求比较高的应用底层的游戏引擎也是基于C/C++实现的。
这些开发者们不采用Java的原因是因为Java在Android平台上的编译和运行方式有待提高。Android为了提升应用的运行速度一直在尝试升级其应用运行机制。从最早的仅仅解释执行字节码到引入JIT编译机制到当前版本的ARTAndroid Runtime支持AOT、JIT和基于画像的编译机制。尽管如此Android对应用的支持仍然存在明显的短板。
第一个短板是垃圾收集机制。我们知道Java基于标记-拷贝算法的垃圾收集机制有两个缺陷。一是要占据更多的内存,二是在垃圾收集的时候会有停顿,导致应用不流畅。在系统资源紧张的时候,更是会强制做内存收集,引起整个系统的卡顿。
实际上Java的内存管理机制使得它一直不太适合编写客户端应用。就算在台式机上用Java编写的客户端应用同样会占用很大的内存并且时不时会有卡顿。你如果使用过Eclipse和IDEA应该就会有这样的体会。
第二个短板是不同语言的融合问题。Android系统中大量的底层功能都是C/C++实现而Java应用只是去调用它们。比如图形界面的绘制和刷新是由一个叫做Skia的库来实现的这个库是用C/C++编写的各种窗口控件都是在Skia的基础上封装出来的。所以用户在界面上的操作背后就有大量的JNI调用。
问题是Java通过JNI调用C语言的库的时候实现成本是很高的因为两种不同语言的数据类型、调用约定完全不同又牵涉到跨语言的异常传播和内存管理所以Java不得不通过虚拟机进行昂贵的处理效率十分低下。
据调查95%的顶级移动应用都是用Java和C、C++等混合开发的。所以,让不同语言开发的功能能够更好地互相调用,是一个具有普遍意义的问题。
第三个短板就是Android的运行时一直还是受Java虚拟机思路的影响一直摆脱不了虚拟机。虚拟机本身要占据内存资源和CPU资源。在做即时编译的时候也要消耗额外的资源。
那么如何解决这些问题呢?我们来看看方舟编译器的解决方案。
方舟编译器的解决方案
方舟编译器的目标并不仅仅是为了替代Android上的应用开发和运行环境。但我们可以通过方舟是如何解决Android应用开发的问题来深入了解一下方舟编译器。
我们先来看看,方舟编译器是怎么解决垃圾收集的问题的。
不过在讨论方舟的方案之前我们不妨先参考一下苹果的方案做个对照。苹果采用的开发语言无论是Objective-C还是后来的Swift都是采用引用计数技术。引用计数可以实时回收内存垃圾所以没有卡顿。并且它也不用像标记-拷贝算法那样,需要保留额外的内存。而方舟编译器,采用的是跟苹果一样的思路,同样采用了引用计数技术。
当然,这里肯定会有孰优孰劣的争论。我们之前也讲过,采用引用计数法,每次在变量引用对象的时候都要增加引用计数,而在退出变量的作用域或者变量不再指向该对象时,又要减少引用计数,这会导致一些额外的性能开销。当对象在多个线程之间共享的时候,增减引用计数的操作还要加锁,从而进一步导致了性能的降低。
不过,针对引用计数对性能的损耗,我们可以在编译器中通过多种优化算法得到改善,尽量减少不必要的增减计数的操作,也减少不必要的锁操作。另外,有些语言在设计上也会做一些限制,比如引入弱引用机制,从而降低垃圾收集的负担。
无论如何,在全面考察了引用计数方法的优缺点以后,你仍然会发现它其实更适合开发客户端应用。
关于第二个问题也就是不同语言的融合问题。华为采取的方法是让Java语言的程序和基于C、C++等语言的程序按照同一套框架做编译。无论前端是什么语言,都统一编译成机器码,同时不同语言的程序互相调用的时候,也没有额外的开销。
下图是方舟编译器的文档中所使用的架构图。你能看到它的设计目标是支持多种语言都统一转换成方舟IR然后进行统一的优化处理再生成机器码的可执行文件。
方舟编译器架构示意图
这个技术方案其实非常大胆。它不仅解决了不同语言之间的互相调用问题也彻底抛弃了植根于JVM的虚拟机思路。方舟编译器新的思路是不要虚拟机最大程度地以机器码的方式运行再加上一个非常小的运行时。
我说这个技术方案大胆是因为方舟编译器彻底抛弃了Java原有的运行方案包括内存布局、调用约定、对象结构、分层编译机制等。我们在第二个模块讲过Graal在仍然基于JVM运行的情况下JIT只是尽力做改良它随时都有一个退路就是退到用字节码解释器去执行。就算采用AOT以后运行时可以变得小一些但Java运行机制的大框架仍然是不变的。
我也介绍过GraalVM支持对多种语言做统一编译其中也包含了对C语言的支持并且也支持语言之间的互相调用。但即便如此它仍是改良主义它不会抛弃Java原来的技术积累。
而方舟编译器不是在做改良而是在做革命。它对Java的编译更像是对C/C++等语言的编译抛弃了JVM的那一套思路。
这个方案不仅大胆而且难度更高。因为这样就不再像分层编译那样有退路方舟编译器需要把所有的Java语义都静态编译成机器码。而对于那些比较动态的语义比如运行时的动态绑定、Reflection机制等是挑战比较大的。
那方舟编译器目前的成果如何呢根据华为官方的介绍方舟编译器可以使安卓系统的操作流畅度提升24%响应速度提升44%第三方应用操作流畅度提升高达60%。这就是方舟编译器的厉害之处,这也证明方舟编译器的大胆革新之路是走对了的。
我们目前只讨论了方舟编译器对Android平台的改进。其实方舟编译器的目标操作系统不仅仅是Android平台它本质上可移植所有的操作系统也包括华为自己的鸿蒙操作系统。对于硬件平台也一样它可以支持从手机到物联网设备的各种硬件架构。
所以,你能看出,方舟编译器真的是志存高远。它不是为了解决某一个小问题,而是致力于打造一套新的应用开发生态。
好了,通过上面的介绍,你应该对方舟编译器的定位有了一个了解。接下来的问题是,方舟编译器的内部到底是怎样的呢?
方舟编译器的开源项目
要深入了解方舟编译器还是必须要从它的源代码入手。从去年9月份开源以来方舟编译器吸引了很多人的目光。不过方舟编译器是逐步开源的由于开放出来的源代码必须在知识产权等方面能够经得起严格的审查因此到现在为止我们能看到的开源版本号还只是0.2版,开放出来的功能并不多。
我参照方舟的环境配置文档在Ubuntu 16.04上做好了环境配置。
注意:请尽量完全按照文档的要求来配置环境,避免出现不必要的错误。不要嫌某些软件的版本不够新。
接着你可以继续根据开发者指南来编译方舟编译器本身。方舟编译器本身的代码是用C++写的需要用LLVM加Clang编译这说明它到目前还没有实现自举。然后你可以编译一下示例程序。比如用下面的四个命令可以编译出HelloWorld样例。
source build/envsetup.sh; make; cd samples/helloworld/; make
这个“hellowold”目录原来只有一个HelloWorld.java源代码经过编译后形成了下面的文件
如果你跟踪查看编译过程,你会发现中间有几步的操作:
第一步执行java2jar这一步是调用Java的编译器把Java文件先编译成class文件然后打包成jar文件。
补充java2jar实际上是一个简单的脚本文件你可以查看里面的内容。
第二步执行jbc2mpl也就是把Java字节码转换成Maple IR。Maple IR是方舟编译器的IR我下面会展开介绍。编译后生成的Maple IR保存到了HelloWorld.mpl中。
第三步通过maple命令执行mpl2mpl和mplme这两项对Maple IR做分析和优化的工作。这其中很重要的一个步骤就是把Java方法的动态绑定用vtable做了实现并生成了一个新的Maple IR文件HelloWorld.VtableImpl.mpl。
最后一步调用mplcg命令将Maple IR转换成汇编代码保存到一个以.s结尾的文件里面。
注意我们目前还没有办法编译成直接可以执行的文件。当前开源的版本既没有编译器前端部分的代码也没有后端部分的代码甚至基于Maple IR的一些常见的优化比如内联、公共子表达式消除、常量传播等等都是没有的。目前开源的版本主要展现了Maple IR以及对Maple IR做的一些变换比如转换成SSA格式以便进行后续的分析处理。
到这里你可能会有一点失望因为当前开放出来的东西确实有点少。但是不要紧方舟编译器既然选择首先开放Maple IR的设计这说明Maple IR在整个方舟编译器的体系中是很重要的。
事实也确实如此。方舟编译器的首席科学家Fred Chow周志德先生曾发表过一篇论文The increasing significance of intermediate representations in compilers。他指出IR的设计会影响优化的效果IR的调整会导致编译器实现的重大调整。他还提出如果不同体系的IR可以实现转换的话就可以加深编译器之间的合作。
基于这些思想方舟编译器特别重视IR的设计因为方舟编译器的设计目标是将多种语言翻译成统一的IR然后共享优化算法和后端。这就要求Maple IR必须要能够兼容各种不同语言的差异性才行。
那接下来我们就具体看看Maple IR的特点。
志存高远的Maple IR
方舟开放的资料中有一个doc目录Maple IR的设计文档就在其中。这篇文档写得很细致从中你能够学习到IR设计的很多思想值得仔细阅读。
文档的开头一段指出由于源代码中的任何信息在后续的分析和优化过程中都可能有用所以Maple IR的目标是尽可能完整地呈现源代码中的信息。
这里我想提醒你注意不要忽略这一句话。它作为文档的第一段可不是随意而为。实际上像LLVM的作者Chris Lattner就认为LLVM的IR损失了一些源代码的信息而很多语言的编译器都会在转换到LLVM IR之前先做一个自己的IR做一些体现自己语言特色的分析工作。为了方便满足这些需求他后来又启动了一个新项目MLIR。你可以通过这篇论文了解Lattner的观点。
Maple IR则在一开头就注意到了这种需求它提供了对高、中、低不同层次的IR的表达能力。我们在分析别的编译器的时候比如Graal的编译器也曾经讲过它的IR也是分层次的但其实Graal对特定语言的高层次IRHIR的表达能力是不够强的。
HIR特别像高级语言特定于具体语言的分析和优化都可以在HIR上进行。它的特点是提供了很多语言结构比如if结构、循环结构等因为抽象层次高所以IR比较简洁。
与Graal和V8一样Maple IR也用了一种数据结构来表达从高到低不同抽象层次的操作。不过不同于Graal和V8采用了图的结构Maple IR采用的是树结构。在HIR这个层次这个树结构跟原始语言的结构很相似。这听上去跟AST差不多。
随着编译过程的深化抽象的操作被Lower成更低级的操作代码也就变得更多同时树结构也变得越来越扁平最后变成了指令的列表。
那么既然Maple IR是用同一个数据结构来表达不同抽象层次的语义它是以什么来划分不同的抽象层次呢答案是通过下面两个要素
被允许使用的操作码:抽象层次越高,操作码的种类就越多,有一些是某些语言特有的操作。而在最低的层次,只允许那些与机器码几乎一一对应的操作码。
代码结构:在较高的抽象层次上,树的层级也比较多;在最低的抽象层次上,会变成扁平的指令列表。
再进一步Maple IR把信息划分成了两类。一类是声明性的信息用于定义程序的结构比如函数、变量、类型等这些信息其实也就是符号表。另一类是用于执行的代码它们表现为三类节点叶子节点常量或存储单元、表达式节点、语句节点。
我用一个简单的示例程序Foo.java带你看看它所生成的Maple IR是什么样子的。
public class Foo{
public int atLeastTen(int x){
if (x < 10)
return 10;
else
return x;
}
public int exp(int x, int y){
return x*3+y+1;
}
}
示例程序编译后会生成.mpl文件这个文件是用文本格式来表示Maple IR你甚至可以用这种格式来写程序然后编译成可执行文件当这个格式被读入方舟编译器后就会变成内存格式另外Maple IR也可以表示成二进制格式到这里你是不是会有似曾相识的感觉会联想到LLVM的IR对了LLVM也可以用三种方式来表示IR文本格式内存格式二进制格式)。
打开.mpl文件你首先会在文件顶部看到一些符号表信息包括类方法等符号
javaclass $LFoo_3B <$LFoo_3B> public
func &LFoo_3B_7C_3Cinit_3E_7C_28_29V public constructor (var %_this <* <$LFoo_3B>>) void
func &LFoo_3B_7CatLeastTen_7C_28I_29I public virtual (var %_this <* <$LFoo_3B>>, var %Reg3_I i32) i32
var $__cinf_Ljava_2Flang_2FString_3B extern <$__class_meta__>
func &MCC_GetOrInsertLiteral () <* <$Ljava_2Flang_2FString_3B>>
接下来就是每个方法具体的定义了。比如exp方法对应的IR如下
func &LFoo_3B_7Cexp_7C_28II_29I public virtual (var %_this <* <$LFoo_3B>>, var %Reg3_I i32, var %Reg4_I i32) i32 {
funcid 48155 #函数id
var %Reg2_R43694 <* <$LFoo_3B>>
var %Reg0_I i32 #伪寄存器
var %Reg1_I i32
dassign %Reg2_R43694 0 (dread ref %_this)
#INSTIDX : 0||0000: iload_1
#INSTIDX : 1||0001: iconst_3
dassign %Reg0_I 0 (constval i32 3)
#INSTIDX : 2||0002: imul
dassign %Reg0_I 0 (mul i32 (dread i32 %Reg3_I, dread i32 %Reg0_I))
#INSTIDX : 3||0003: iload_2
#INSTIDX : 4||0004: iadd
dassign %Reg0_I 0 (add i32 (dread i32 %Reg0_I, dread i32 %Reg4_I))
#INSTIDX : 5||0005: iconst_1
dassign %Reg1_I 0 (constval i32 1)
#INSTIDX : 6||0006: iadd
dassign %Reg0_I 0 (add i32 (dread i32 %Reg0_I, dread i32 %Reg1_I))
#INSTIDX : 7||0007: ireturn
return (dread i32 %Reg0_I)
}
这里我给你稍加解释一下示例代码中的IR。
以func关键字开头定义一个函数。函数名称里体现了原来Java类的名称和方法名称。public和virtual关键字也是继承自原来的Java方法在Java中这类public的方法都是virtual的需要动态绑定。
接下来要注意的是用var声明以%开头的三个伪寄存器。伪寄存器相当于本地变量,它的数量是无限的,在后端做寄存器分配的时候才对应成物理寄存器。
在这后面的是6个dassign语句。这是6个赋值语句其中的d是直接寻址的意思。有的dassgin操作符后面跟着的是常数constval有的跟着的是加法add或乘法mul表达式。而加法和乘法表达式里面又可能进一步用到其他的表达式。这里就体现出了Maple IR的特点即它是树状结构的。
那么总结起来示例函数体现了Maple IR最基本的结构特点程序被分成一个个的函数。函数里呢是顺序的一条条语句而每条语句都是一个树状结构树的节点可以是叶子节点、表达式或者其他的语句。如果把函数内的每条语句作为函数的子节点那么整个函数就是一个树状的数据结构。
另外,在示例程序中,还有一些以#开头的注释。这些注释代表了原来class文件中的字节码。目前方舟编译器里有一个字节码的前端能够把字节码翻译成Maple IR。这个注释就体现了字节码和Maple IR的对应关系。
不过上面的示例函数并没有体现出流程控制类的语句。我们再来看一下atLeastTen()方法对应的IR。atLeastTen()方法中有一个if语句它能否被翻译成Maple IR的if语句呢
func &LFoo_3B_7CatLeastTen_7C_28I_29I public virtual (var %_this <* <$LFoo_3B>>, var %Reg3_I i32) i32 {
funcid 48154
var %Reg2_R43694 <* <$LFoo_3B>>
var %Reg0_I i32
dassign %Reg2_R43694 0 (dread ref %_this)
#INSTIDX : 0||0000: iload_1
#INSTIDX : 1||0001: bipush
dassign %Reg0_I 0 (constval i32 10)
#INSTIDX : 3||0003: if_icmpge
brtrue @label0 (ge i32 i32 (dread i32 %Reg3_I, dread i32 %Reg0_I))
#INSTIDX : 6||0006: bipush
dassign %Reg0_I 0 (constval i32 10)
#INSTIDX : 8||0008: ireturn
return (dread i32 %Reg0_I)
@label0 #INSTIDX : 9||0009: iload_1
#INSTIDX : 10||000a: ireturn
return (dread i32 %Reg3_I)
}
在Maple IR中提供了if语句其语法跟C语言或Java语言的语法差不多
if (<cond-expr>) {
<then-part> }
else {
<else-part>}
像if这样的控制流语句还有doloop、dowhile和while它们都被叫做层次化的控制流语句。
不过在阅读了atLeastTen()对应的IR以后你可能要失望了。因为这里面并没有提供if语句而是通过一个brture语句做了跳转。brtrue被叫做平面化的控制流语句它在满足某个条件的时候会跳转到另一个语句去执行。类似的控制流语句还有brfalse、goto、return、switch等。
补充:由于这个.mpl文件是从字节码直接翻译过来的但字节码里已经没有HIR级别的if结构了而是使用了比较低级的if_icmpge指令所以方舟编译器也就把它翻译成了同样等级的brtrue指令。
好了通过这样的示例你就直观地了解了Maple IR的特点。那么问题来了当前的开源项目都基于Maple IR做了哪些处理呀
你可以打开源代码中的src/maple_driver/defs/phases.def文件。这里面定义了一些对Maple IR的处理过程。比如
classhierarchy对类的层次结构进行分析
vtableanalysis为实现动态绑定而做的分析
reflectionanalysis对使用Reflection的代码做分析以便把它们静态化。
ssa把IR变成SSA格式
……
总的来说当前对Maple IR的这些处理有相当一部分是针对Java语言的特点来做一些分析和处理以便把Java完全编译成机器码。更多的分析和优化算法还没有开源我们继续期待吧。
课程小结
这一讲我主要跟你探讨了方舟编译器的定位、设计思路以及方舟编译器中最重要的数据结构Maple IR。
对于方舟编译器的定位和设计思路我认为它体现了一种大无畏的创新精神。与之相比脱离不了JVM模子的Android运行时倒有点裹足不前使得Android在使用体验上多年来一直有点落后。
但在大胆创新的背后也必须要有相应的实力支撑才行。据我得到的资料华为的方舟编译器依托的是早年在美国设立的实验室所积累下来的团队这个团队从2009年开始就依托编译技术做了很多研发为内部的芯片设计也提供了一种语言。最重要的是在这个过程中华为积累了几百人来做编译和虚拟机的团队。前面提到的首席科学家周志德先生就是全球著名的编译技术专家曾参与了Open64项目的研发。这些优秀的专家和人才是华为和国内其他团队未来可以在编译技术上有所作为的基础。那么我也非常希望学习本课程的部分同学以后也能参与其中呢。
对于Maple IR中分层设计的思想我们在Graal、V8等编译器中都见到过。Maple IR的一个很大的优点就是对HIR有更好地支持从而尽量不丢失源代码中的信息更好地用于分析和优化。
对于方舟编译器根据已开源的资料和代码我们目前只做了一些初步的了解。不过只分享这么多的话我觉得还不够满意你也会觉得很不过瘾。并且你可能还心存了很多疑问。比如说Graal和V8都选择了图的数据结构而Mapple IR选择了树。那么在运行分析和优化算法上会有什么不同呢我希望后续随着方舟编译器有更多部分的开源我会继续跟你分享
这节课的思维导图我也放在了这里,供你参考:
一课一思
你认为用一种IR来表示所有类型的语言的话都会有哪些挑战你能否通过对Maple IR的阅读找到Maple IR是如何应对这种挑战的欢迎在留言区分享你的观点。
如果你身边也有对华为的方舟编译器十分感兴趣的朋友,非常欢迎把这节课的内容分享给他,我们一起交流探讨。感谢你的阅读,我们期末答疑再见!

View File

@ -0,0 +1,163 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
不定期加餐1 远程办公,需要你我具备什么样的素质?
你好,我是宫文学。到这里,咱们课程的第一模块“预备知识篇”就已经更新完了。通过这么多讲的学习,这些编译技术的核心基础知识,你掌握得怎么样了呢?是不是感觉自己已经构建了一个编译原理的知识框架了?
不过我也知道,要理解编译技术的这些核心概念和算法,可能不是一件很容易的事儿,在跟随我一起探索编译之旅的过程中,估计也耗费了你不少的脑细胞,那咱们是时候来轻松一下了。
今天,我就想借着这个加餐的环节,跟你聊一聊一个很有意思的话题:远程办公。
之所以选择这个话题,主要有两方面的原因。
首先,由于疫情的影响,春节之后,很多公司都采取了远程办公的方式。所以,如何在远程办公的情况下做好工作,对于员工和公司来说,都是一个挑战。
第二个原因,是我个人一直对于远程办公这种工作模式很感兴趣,这些年来也一直在做这方面的思考,关注这方面的实践,所以有了一些心得体会,想跟你分享一下。
不过,要想把远程办公这个话题聊清楚,确实不容易,分歧也比较大。有一些朋友会比较悲观,觉得远程办公根本不切实际;而另一些朋友则会很乐观,觉得远程办公马上就会普及。
今天,我就来分享一下我看待远程办公的一些视角。我会从公司和员工这两个角度,来分析远程办公带来的机遇和挑战,希望能给你带来一些启发,让你以更积极和务实的姿态,迎接远程办公的浪潮,甚至在这种工作模式转换的趋势下,抓住更多的发展机遇。
首先,我来聊一聊远程办公的那些吸引人的地方。
远程办公的好处
我对远程办公的了解最早是透过开源圈的朋友了解了一些故事后来自己也接触了一些酷酷的公司。很多做开源软件产品和技术服务的公司都是远程办公的他们的员工可能来自世界各地。我曾经接触过一个芬兰公司的CEO他们主要做嵌入式Linux的技术服务。一百多人的公司平常办公室是没什么人的。据说他们公司有的员工可以一边上班一边去全世界旅游。
我当时认为一百多人的公司全部都能远程办公并且管理良好就已经很不错了。但后来看了一篇文章讲到WordPress的母公司Automattic有上千名员工分布在全球75个国家全部都是远程办公。这就有点令人吃惊了我才意识到在互联网时代原来远程办公可以适用于任何规模的企业。
这次疫情中IT领域的很多公司都大量地采用了远程办公模式包括谷歌、Facebook、微软等大型企业。
现在新闻上说,疫情之后,世界再也回不到过去了。其实我觉得,在很多领域,我倒是宁愿它回不去了。比如,远程教育;再比如,远程工作。
因为远程,意味着你获得了一个难得的自由:位置自由。
现代社会,我们苦“位置”久已!因为很多资源都是跟位置绑定在一起的,比如说,教育资源与学区房。
我在北京的很多朋友,他们在孩子上学期间,一直都是租房子住的,因为要住得离孩子学校近,而自己的房子会租出去。这种状态要持续到孩子上大学为止。
而对于若干都市白领来说,在大城市上班,就意味着要把整个肉体在办公室和家之间移动,所以每天可能要在路上花两三个小时,很多时候还会在路上堵个半天。
如果我们真的获得了位置自由,那么整个生活的幸福指数会提高一大截吧!
对于远程教育来说,我比较希望见到的现象,是在偏远的乡村,一样能够通过线上教育获得最优质的知识资源。至于线下的老师,更多的是关注孩子的健康成长,多带着孩子亲近大自然,扮演“育”的角色,而不是“教”的角色。
工作也是一样。现在越来越多的工作,都可以在网上进行了。互联网电商的发展,虽然让一些线下店铺的营业状况受到了影响,但只要能通网络,很多人在网上就可以卖东西了呀。另外,随着外卖的兴起,很多餐饮企业也不再需要临街的店面了。
所以,通过远程办公,我们可能就不需要北漂、深漂等各种漂了,可以选择离自己的亲人更近一些,或者可以反过来,四海为家。并且,你还可能获得更多、更好的工作机会,你可以从全世界的公司里选择你喜欢的那份工作,并且也不需要离开你喜欢居住的地方。
并且,伴随着位置自由,往往也会给我们带来时间自由。因为远程后不再需要按时上班打卡了(很多在全球都招揽人才的公司,大家的作息时间都不一样,当然不可能统一打卡),所以管理体系会更加面向绩效,而不会管你到底是在什么时间来完成这些工作的(通常也没法管理)。这就意味着,你可以在家人需要你的时候,出现在他们身边(比如接孩子),然后选择自己合适的时间段来工作。
上面说的是远程办公对员工的好处。从企业的角度来看,远程办公其实也会带来一些潜在的好处。
首先,有些员工可能会在工作上做更多的投入(这跟某些员工会偷懒恰恰相反,所以可能出现两极分化)。这些人很享受自己的工作,每天上下班反倒降低了他可能做出的贡献。如果公司有一套良好的管理体系,那就可能会因此筛选出更适合自己的员工,而避开那些混日子、划水的员工,整个团队的素质反倒会得到提高。
我曾经跟MySQL的前CEO Mårten Mickos聊天。我问他管理远程办公的员工需要注意些什么
他思考了一下,说要建议员工跟家人一起住,至少要养条狗什么的。因为家人能帮助管理这些极客们的作息。不然由着这些极客们的性子,他们会昏天黑地、毫无规律地作息,不利于健康。就算养条狗,你也会因为要照料它们,而让自己的生活节奏健康一点。
他的回答其实出乎我的意料,我原本以为他会说什么公司的管理措施之类的。你体会一下,如果你是公司老板,你是不是会因为拥有这样热爱工作的员工而感到欣慰呢?
第二,因为没有了地域限制,公司也就可以充分任用全球各地的人才。这个方面在很多做开源软件的公司那里,得到了很好的体现。如果你喜欢某个开源产品,在社区里贡献自己的力量,那你很可能就会被邀请加入到该公司。
在互联网时代企业的组织方式也正在重构。滴滴打车、美团外卖这些采用新雇佣方式的公司不但可以更好地利用各地的人力资源TA们也提供了一些自由工作的机会。
第三,没有了地域的限制,公司也可能更容易拓展自己的市场。这个好处也是显而易见的,就不用我多说了。
远程办公的挑战
上面是我对远程的一些美好的憧憬。还是回到现实吧,因为更改现有的教育体制,可能是很难的。而让企业老板们改变公司的管理方式,难度也不低。
老板们都是理性的。真金白银投入做企业,是要见到效益的。可是,如何能保证采用远程办公模式,不会让企业变成一团散沙,纪律涣散、效率低下呢?
你可以问问,在春节后不得已实行远程办公的企业,对经营有没有产生影响。
说实话,在没做好充分的准备之前,仓促地采用远程办公,肯定会产生各种不适。
因为远程办公,对于管理体系,有着更高的要求。很多工作是难以直接度量绩效的,比如说研发工作就比销售工作更难衡量绩效。
而没有良好的管理体系仅凭员工的自觉是不可能产生良好的效果的。其实硅谷有一些IT公司很早就实行过远程办公但后来又取消了原因就是绩效不理想混日子的员工太多。
反过来,站在员工的角度,你真能做好自己的工作管理吗?在办公室工作的时候,迫于同事们的目光,你总得做点事情吧。可是,如果脱离了直接的监督,有多少人能够真正管好自己呢?
你说你的自我管理能力强那么请问有多少人能控制住自己每天刷手机的时间呢据说超过50%的成年人,都有手机上瘾症。在办公室的时候,尚且见缝插针地刷手机。如果在家办公,又会怎样呢?
有过远程工作经历的人都会经历这么一个时期。即使是你很有责任心、很有事业心但也要每天花费很多的精力来管理自己的行为。我认识的一个朋友她在IT行业主要做售前支持工作。之前跟她闲聊的时候她说自己花了3年的时间跟自己搏斗才养成了良好的居家工作习惯。而管理自己这件事情也是消耗注意力的。注意力本身又是个稀缺资源。所以在初期你会觉得对比在办公室里居家办公会更累在公司你不需要花精力来控制自己的行为因为环境和同事帮忙做了这件事情实际上节省了你的注意力。
我也听说,有的工程师会在网上直播自己编码的过程。这样做的一个原因,就是为了帮助管理自己的行为,因为这时候你必须更加集中注意力在自己的工作上。
还有一个是办公环境的因素。我们中国人的居住状态比较拥挤,在自己家里开辟出一个安静的、不被打搅的书房并不容易,这可能还跟中国的文化有关。而西方的文化,可能会更尊重个人的空间。
再说了,我们跟外国人的居住条件也确实不同。西方发达国家很早就开始了郊区化发展,大部分人会住在郊区和小城镇,自然环境比较好。而我们中国呢,大部分住在小区的楼房里。
不过,如果真的远程工作了,你也可以不住在大城市呀。网上有些视频经常会吸引我,某夫妇在乡村翻新出一栋漂亮的别墅,还拥有美丽的花园,等等。其实我目前就住在一个自然环境良好的山上,只不过从这个村子去办公室也很方便就是了。
远程办公还会产生心理上的挑战:白天晚上都在家里,会容易心理疲劳。而换个环境,反倒会让人兴奋起来。我就有个感觉,在家里工作久了,效率就会降低。而这时候再回公司工作的话,反倒更容易集中注意力。
而且,远程办公肯定也会降低沟通的效率。一些互联网公司,在设计办公室的时候,会故意设计一些空间,方便大家偶遇,闲聊几句。而做研发工作的同学都知道,这种看似随意的交流,有时候能激发出很多创新的思维。而如果总是自己苦思冥想,往往很快就会走入死胡同。这种线下偶遇式的沟通,往往见到了就会聊个几句。但在远程办公时,如果大家互相见不到面,还真就不聊了。
面对远程办公,我们要做好什么准备?
所以,我们需要实行一些积极的操作,来更好地应对远程办公给我们带来的挑战,这样也能更好地抓住远程办公给我们带来的机遇。
从公司的角度出发
那首先,我们来看看,对于企业来说,都需要做好什么准备。
第一,我觉得企业管理者要建立一个意识:远程办公是企业必须要面对的管理考验。
其实只要企业做大了,几乎都要面对远程管理的问题。比如你有了分公司,或者在各个城市设门店,甚至把生意做到国外。那么,突破地域的限制拓展业务,本来就是对企业能力的考验,是企业发展中必须踏过的门槛。
所以说,企业也一样需要获得位置自由。这些分公司、派出机构工作的人员,对于总部来说,本来就是远程工作的。有了这个意识,管理者就会开始放弃旧的思维,拥抱远程办公。
第二,从看住人,转换到管绩效。
很多比较传统的企业,他们的绩效标准都比较模糊,所以在远程办公的形势下,我们需要把绩效标准的清晰化、准确化放到第一位。像滴滴、外卖这些新职业,之所以能够迅速扩展规模,充分利用社会化人力资源,就是因为他们的工作绩效的标准是清晰而准确的。
第三,建立拥抱远程办公的文化,给员工授权和赋能。
像软件研发类的工作,它是知识密集型的,对员工的绩效评估比较难,人员更换的成本也相对较高。那么对于这类工作,我们可以多向那些开源软件公司学习,建立一个拥抱远程办公的公司文化,去吸引那些对工作充满兴趣和热爱的人参与进来。这些人,也会更加珍视公司给予的授权和自由。
第四充分利用IT技术。
管理一定要落实在工具上。我接触的那家芬兰公司就花了很多年的时间积累了一套成熟的内部管理系统。比如说作为软件公司你肯定要对项目进度、代码量、Bug数等基础指标有所管理才行吧
信息技术成本的降低也大大降低了远程管理的开销。这次疫情促进了视频会议在全世界的普及。对于中国的中小企业来说甚至可以0成本享受高品质的远程会议服务这真是一个了不起的福利
从员工的角度出发
OK说完了公司那我们再来看看从员工的角度出发我们都要具备什么素质才能更好地迎接远程办公模式。
第一,员工也要建立一个意识:无论是否远程办公,都要向绩效负责,管理好自己的工作。
即使你仍然在传统的办公模式下工作,如果你能像一个远程工作者那样对绩效负责,管理好自己的注意力,我想你很快就会获得领导的注意,从而赋予你更大的工作自由。你有没有听说过,张小龙经常睡懒觉迟到,而马化腾从来不管他?因为马化腾需要的是一个能做出微信来的张小龙,而不是一个每天按时打卡的张小龙。
第二,正视远程办公对自我管理的高要求,养成良好的工作习惯。
在办公室工作,会有环境来约束你。而当真的给了你位置自由以后,你其实要珍视这种自由,给自己定一些规矩,甚至给自己找一些监督(就像前面说的在网上直播),从而养成良好的工作习惯。
第三,建立激进的协作习惯。
由于远程工作对于协作的挑战,你必须建立激进的协作习惯,而不是仅仅停留在我们目前使用即时通讯工具和视频会议工具的习惯上。比如,你可以全时间视频在线、主动找人线上闲聊一小会儿、主动创造一些与人沟通的机会,等等。
第四,可能是最重要的:为兴趣而工作,为自己而工作。
人在没有很多督促的情况下,真正能驱动自己前行的动力,就是兴趣了。这个时候,你会把工作看作是促进自己成长的必要因素,从工作中成长,从成长中获得快乐。这个时候,你已经不是在为公司工作,而是为自己而工作。这样的人,才算获得了真正的自由。
小结
今天,我们讨论了远程办公对公司和员工的好处、挑战,以及我们需要做好的准备。我讲了两个主要的观点。第一个观点是对企业的:远程办公管理能力是企业未来必须具备的能力。第二个观点是对个人的:只有能够管理好自己的人,才能抓住远程办公带来的机遇。
那总体来说呢,信息技术的进步是不可阻挡的,它对于工作和生活方式的重塑,也绝不会停止。所以,远程办公一定会越来越成为现实,不管是对于企业还是员工,我们都要积极拥抱这样的未来。
讨论一下
你有想过,有一天会通过远程办公的方式上班吗?远程办公有哪些地方会吸引你?你有通过远程办公的方式上班的朋友吗?你觉得,为什么远程办公现在还没有真正普及开呢?你可以在留言区里,跟大家交流交流你的看法。
感谢你的阅读,欢迎你把今天的内容分享给更多的朋友。

View File

@ -0,0 +1,172 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
不定期加餐2 学习技术的过程,其实是训练心理素质的过程
你好,我是宫文学。
最近,高考刚刚结束。每年一度的高考都牵动了很多人的心,学生和家长们都把高考看作是人生的一大关键关口。可是,等上了大学以后呢?很多同学也会感到不适应,因为缺少了一个像高考那样明确的学习目标,也没有老师和家长在旁边不停地鞭策和关注。到了工作以后,就更是如此了。
对于进入计算机领域的人而言呢,很多人迫于找一份好工作的压力,会刻苦学习一段时间,包括参加各种学习班。而一旦获得了一份工作,融入职场以后,很容易就进入舒适区。反正当前的工作也能应付过去,为什么还要费力再去学呢?毕竟,工作已经够辛苦了。
在这种情况下,人生的第二次转折点就出现了。
有的人能够管理好自己充分利用各种时间和机会不断地加深自己对技术的理解。虽然短时间看上去进步并不大但成年累月地积累下来效果就逐渐出现了TA们开始能够胜任一些关键岗位成了技术领头人。而另一些人则只能掌握那些比较容易掌握的技术时间一长就会显得平庸等年轻人赶上来的时候就更加没有竞争优势了。虽然这不是像高考一样能马上分出重点大学和普通大学的差别来但在进入职场5年、10年以后这两类人在发展上的差别并不比高考带来的差别小。
我说这些,不是在贩卖焦虑,而是想引出我们今天要讨论的话题:从心理的角度看待学习技术的过程。特别是自己管理自己学习的过程、跟自己相处的过程。
学习没有轻松的。尤其是学习像编译原理这样的基础技术,就会显得挑战更大。想要学好它,调整和保持一个良好的心态是非常重要的。而通常,我们在心理上会面对三大问题:
第一,我为什么要学习这么难的技术?学一些比较容易的、应用层面的技术不就行了吗?这是学习的目的和动力问题。
第二,以我的能力,能学会这么难的技术吗?这是自信心和勇气的问题。
第三,如何看待学习中经常遇到的挫折?总是找不到时间怎么办?等等。这是学习过程中的心态调节和习惯养成问题。
如果对这三方面的问题,你都获得了清晰的答案,那么你应该就能保持好持续学习、终生学习的心态,从而对自己的人生有更好的掌控力。
那接下来,我就给你解读一下,我对于这三类问题的理解。
首先,我们来说说学习目的问题。
为什么要学这么难的技术?
在做课程设计的时候,我和编辑同学都会尽量想着如何让这样的基础技术和你的日常工作关联起来,让你觉得它不是屠龙之术,而是能够在日常工作中发挥实际效用的。这确实是学习基础技术的收获之一。
不过,如果想长期坚持下去,我会建议你把心态调整成一种更高级的模式。用中国文化中的一句话来形容,就是“用出世的态度,做入世的事情”。如果一件事情你觉得应该去做,那就去做,不要太斤斤计较一些功利层面的东西。
那么对于学计算机而言什么是我们应该去做的呢那当然是要了解计算机领域的那些最基础的原理呀。如果搞了一辈子IT技术却不明白其中的道理那岂不是一辈子活得稀里糊涂的
我知道,大部分人不注重基础性知识的原因,可能是觉得它们不会马上发挥作用。可是,那些最重要的知识、那些构成你知识结构的底蕴的东西,往往就是那些看上去不会马上有用的东西。
我个人非常欣赏复旦大学做教育的一种态度就是教给学生无用之学。哲学、艺术、写作、演讲、逻辑学、历史等知识在西方教育中被称作Liberal Arts我们有时候翻译成通识教育或者博雅教育。这些教育对于我们从事任何专业的工作都是有用的。
比如说,美学素养。一个设计良好的系统架构,一定是优美的。新东方的元老之一王强,在美国学习计算机的时候,会把写完的程序拉开一定的距离看。如果看上去不够美观,那一定是程序设计得不够好。
你乍一听上去,可能会感觉是无稽之谈,但有经验的程序员一定会认同这个看法。那些写得有问题的程序,往往本身就是又臭又长、非常难读;而高质量的程序,往往是模块划分清晰、简洁易读的。做不出好的系统设计的人,肯定美学素养也不够高。像爱因斯坦等大科学家,往往驱动他们做出某个研究成果的动力,就是想去发现一条更加简洁、更具优美感的公式,因为真理往往是简洁的、优美的。
我之前公司的一名股东,他以前是一位很厉害的软件工程师,后来被一个外企挖走,担任了多年的销售副总。挖他去外企的原因,就是因为当时该外企刚开始在中国推广中间件的概念,他听了介绍以后就说,那不就跟我写的某软件的原理是一样的吗?并且一下子就说出了这类软件的关键技术点。于是,该外企下定决心要把他挖过去,并且是去负责销售。去年,他突然又写了一套科幻小说,名称是《云球》。我这里不是为他打广告,我是想说,做一个优秀的软件工程师、担任销售副总和小说家,这三个职业从表面上看相差很大,但其实背后依赖的基础素质都是一样的,都是那些乍一看上去没用的基础知识、基础素质。
所以,从这个角度,我是同意素质教育的理念的。一个缺乏美学素养、哲学素养和沟通能力等素质的软件工程师,潜力可能是有限的。
说到基础素养,我补充一个例子。有一次,我和前面说到的这位朋友在一起聊天,结果一个软件公司的老总给我们吹嘘他们公司开发的某软件平台。在说到一些特性的时候,听得我俩目瞪口呆。后来我们告诉这位老总,他声称的东西违背了基本的物理学和信息学的规律。在违背科学的底层规律的方向上做事情,那就相当于去造永动机,根本是虚妄的。这是很多项目失败的根本原因。
而另一些人,却具备抓住事情本质的能力。众所周知,马云并不懂技术。但就是不懂技术的马云,在懂技术的马化腾、李彦宏都认为云计算不是趋势,只不过是新瓶装旧酒的时候,果断拍板做云计算技术。期间,来自内部的反对声一直很强,大家都不愿意在内部使用尚不成熟的云计算技术。然而时间证明,马云的眼光更准。并且,力主开发云计算技术的王坚博士,他自己的专业也不是计算机专业。那么,为什么一拨非科班人士会比科班的技术大佬们看问题还准呢?我想可能是他们的无用之学学得更好,基础素质更全面吧。
所以这就是我对于像编译原理、操作系统、算法等基础知识的态度。你就把它们看做是无用之学好了。我不仅鼓励你把这些基础知识学明白并且我也希望你可以尽量再往深里挖一挖。比如像图灵那样去思考一下计算的本质到底是什么编译原理用到的形式语言也可以被继续深挖从而跟整个西方科学体系底层的形式逻辑体系挂钩以此去深入地理解希尔伯特猜想和哥德尔定理了解面向对象、函数式编程这样的编程范式跟人类的认知和思维模式的关系跟Lamda计算、范畴论等数学工具的关系你还可以去了解复杂科学领域的成果并用这样的思维去看待大型复杂的信息系统。
如果你觉得编译原理这样的技术没啥用,那你一定会觉得我刚才说的那些更加没用。但我知道,一个优秀的软件工程师,其实一定是对我说的那些话题有所涉猎、有兴趣的。
总结起来,一个人的基础素质,决定了他的思维方式、思维质量和眼光,那些看上去没用的基础知识、基础原理,其实是真正做大事、承担重任所需要的素质。那,你到底要不要去学习呢?
好,如果你认可我的观点,那么我们接下来再探讨第二个话题,关于学习的信心问题。
我能学得会吗?
很多人都会有一个担心,说某些基础技术、基础原理太难,自己的基础又不够好,那么能学得会吗?如果学了半天又学不会,那不是白费力气吗?
从能力角度,我必须承认,我们每个人都是有天赋的差异的。你让一个普通人去跟姚明比赛打篮球,那不是难为人吗?
学习这件事情也一样有天赋的问题。
我本人当年在高考的时候,是省里的前几名,但是等我到了北大,看到周围的同学通常也都是身手不凡;在记忆力方面,我也比不过很多同学,有的同学对普通的词汇书根本不感兴趣,会去背词典,甚至背专业领域的词典;在数学等需要逻辑思维的领域,我又比不过另一些同学,直到今天,对于一些涉及数学的问题,我都会去咨询这些同学的意见。
但从另一个角度讲,一些基础知识、基础原理,一定要有很强的天赋才能学会吗?
不是的。在人类知识的殿堂中,你要想增加一点新的理论、新的原理,那是非常难的。所以我们必须对那些大科学家们,那些计算机领域的先驱们顶礼膜拜。那些顶尖的工作,确实需要天赋,再加上努力和机缘。
不过,即使狭义相对论和广义相对论发明起来那么困难,但一般的理工科学生只要想学,都是可以弄明白的。这就证明了,发现知识和学习知识所需要的能力,是极大的不对称的。在高考季,经常会出现妈妈级、奶奶级的考生,从陪考到变成跟儿孙辈一起上大学的故事。人家奶奶都能考上大学,我们年轻大学生学不会本专业的一些基础原理,这个道理说得通吗?
同理你常常会听到的一个理由也是不成立的这个理由就是我不是科班出身。这个我就不认真去反驳了。你想想看吧费马的本职是律师而他“业余”是个大数学家数学家罗素却获得过诺贝尔文学奖比尔·盖茨进的是哈佛大学商学院我前面说的王坚博士是学心理的罗永浩的专业也肯定跟IT没关系刘慈欣是业余写小说的。
所以,那些所谓的困难,只是你给自己设的玻璃天花板。这不是个能力问题,而是个心理问题。儒家提倡“智、仁、勇”三种最高的道德标准,勇气是其中之一,它也是我们应该训练的一种品质呀。
好,如果你又一次认同了我的观点,那么我们再来讨论第三个问题,如何克服学习过程中的困难。
如何持之以恒?
在我看来,如果理顺了前两个问题,也就是为什么要学,以及信心和勇气的问题,那么你最大的心魔其实就破除了。
但毕竟,学习贵在持之以恒的坚持。在这个过程中,我们可能会遇到很多的困难。但对于这些困难,我们也要用正确的心法来对待。所以,接下来我就针对如何面对学习中的困难、如何保证学习时间、如何找到学习的乐趣等常见问题,谈谈我的看法。
困难是必须的
首先你得明白有价值的东西一定是要克服困难才能得到的这是公平的。所以不要指望学知识而不需要付出努力再好的教程和老师也只是起到辅助作用。这里你得注意一个问题就是不要被某些书籍和课程收了智商税比如说“7天学会XXX”“学英语其实不用背单词”等等。这种标题就是违背学习的基本规律的。
所以,当你知道了苦难不可避免这个道理,那你剩下的就只有面对这些苦难。在学习中,你可能经常会被一个难点阻碍住,这很正常。你正确的心态应该是这样的:
没有我拿不下的山头,正面拿不下从侧面,侧面不行走背面。多换几个角度,多几次尝试,多看点参考资料,总会成功;
那么多人都能学会,我没有道理学不会,一定有更好的方法;
这个问题既然有难度,那价值一定也大,所以一定不要放弃。
有了这样的心态,其实再苦再难的事儿都好说了。
在旅途中发现乐趣
我一个朋友最近正在从新疆骑行到西藏全程3000公里中间需要穿越无人区。这是他第三次做这样的骑行之前已经骑过川藏线、青藏线。虽然过程很艰苦但沿途美丽的风景和跟自己相处的过程就是这个旅途给他的回报。
我自己也喜欢户外。我家人有时不理解我,问我为什么要开着一辆大房车去那么远,累不累呀。我说,这就是旅行的意义呀。如果直接飞机过去,那有什么意思。
我用这两个例子作类比,是想告诉你:当我们学习那些有难度的知识的时候,其实肯定能发现出其中的乐趣来。比如,在学编译原理的时候,你去动手实现几个小例子,哪怕还不到实用的程度,但是好玩呀!当你找到了其中的乐趣,那么别人看你是在艰苦地学习,但其实你是乐在其中呢。就好像,别人看着一个人是在顶风冒雪一个人骑行,但他也是乐在其中呢!
另外呢,在互联网时代,各种不需要动脑的娱乐方式层出不穷。普通的人会在这种廉价的快乐中流连忘返。而如果你的目标是持续进步,那要培养自己另一种习惯,就是习惯于获得那些艰难的乐趣,这种乐趣是真正的充实的乐趣。
跟自己相处
我前面举的朋友骑行的例子,他是自己一个人。我也喜欢自己开车出去,因为没有了其他人,也就避免了因为人际关系而导致的分神,你只需要关注大自然和你自己。你能感受到自己跟自己对话的过程,自己跟大自然对话的过程。
学习在大多数情况下也是一个人前行的过程,学到的知识也只属于你一个人。在这个时候,就只剩下了你要去攻克的知识,和你自己。你能感受到自己跟自己对话的过程,自己跟知识对话的过程。当遇到困难了,你能发现自己的苦闷和焦虑;当解决问题了,你能感受到自己的欣喜。
真正有价值的成绩,都是在这样的跟自己独处、跟自己对话的过程中做出来的。这是一种值得追求的素质。
跟志同道合者相伴
独行难,众行易。除了那些内心特别强大的、从来都不屑于与普通人同行的天才,我们大部分普通人还是愿意有一些同伴一起结伴而行的,这样会大大降低驱动自己所需的努力。
我在读研时曾报过GRE的培训班。我感觉报班的最大作用其实不是跟着老师学到多少知识而是培训班乌泱乌泱的一大堆的同学给我提供了一种气场让我每天不想别的赶紧学习就是了。
这样的群体还会有效改变自己的学习标准。在学GRE之前我觉得一天背几十个单词已经挺辛苦的了。但到了GRE班我很快就接受了每天背200个的新标准因为其他人也是采用这个标准的。关键是就算每天背200个我也没觉得有多困难。所以你看人的潜力有多大的弹性而一个好的群体就是能无形中给人提供这种心理上的能量。
而且那时的同学都会有这种体会,就是每天如果不背单词就不舒服,上瘾。那段时间,随便看到一个单词,脑子里就会出现几个近义词和反义词,这种感觉很奇妙。再次印证了我前面说到的那种奋斗中的乐趣。
在软件领域,有很多技术社区,这些社区也能起到对人的心理加持作用,你可以善加利用。
最后,如果有要好的朋友和导师,能够鞭策你,那也非常难得。有管理经验的人都知道,虽然我们希望每个员工都有自我驱动的能力,但合适的外部驱动能降低员工驱动自己所需要消耗的努力。毕竟,我们大部分人其实是愿意工作在“低功耗模式”,能节省能量就节省能量。
使用运营思维
在互联网时代各种App在功能设计和运营上充满了心理学的套路以便培养用户的习惯。游戏公司更是会雇佣心理学专家来设计各种套路。
那么,与其让别人套路你,不如自己套路自己,同样利用心理学的知识来培养自己的学习习惯,把自己的时间、自己的命运把握在自己手里,不是更好吗?
心理学的基础原理并不难你自己就能从各种App的使用套路里体会到一些。比如说对取得的成绩即时给予奖励。从心理学的角度、从各种App背后的运营者的角度来看我们每个人其实就是巴甫洛夫实验室里的动物而已。通过这样的自我训练你可以达到一些很好的效果
建立良好的学习流程,有明确的开始和结束时间;确认一下每天的学习目标和学习成果,或者可以建立学习过程的仪式感;给自己一个良好的环境。
没有学习的时间?那是不可能的。这是因为你没有给学习安排出专门的时间来。
以输出带动输入。很多同学有写技术博客的习惯,这个习惯非常好。因为你要写出东西来,所以会逼迫自己把思路理清楚。
激进一点的:直播自己的学习过程,给自己提供外部监督和激励机制。
小结
今天这一讲,我聊了聊对于学习比较难的、比较基础的知识的心法的理解。总结起来,主要有三点:
第一,那些基础知识的素养,决定了一个人的发展潜力,这是你要学习它们的原因;
第二,没有学不懂的知识,真正的障碍是心理上的自我设限;
第三,学习的过程,就是砥砺前行的过程,经常能自省和调整自己的状态,就能养成自己的学习能力。
那么,你对于学习,有没有什么好的心法?欢迎在留言区跟大家交流!
感谢阅读。如果你也觉得很有收获,非常欢迎你把今天的内容分享出去,跟你身边的朋友一起做会学习的人。
我们接下来就要进入到期中复习周了,到时候你就可以来看看,在前半段的课程中,你都学习得怎么样!

View File

@ -0,0 +1,122 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
不定期加餐3 这几年,打动我的两本好书
你好,我是宫文学。
在互联网时代,读书好像变成了一件挺奢侈的事情。因为我们现在获取信息的主要渠道,已经变成了网络。不过,在互联网统治力如此强劲的今天,我偶尔仍能发现一些好书,让我可以放下电脑和手机,对着厚厚的一大本,从头看到尾,甚至还会看很多遍。可见这些书确实是真正打动了我,让我这个理科背景的人,能以新的视角来看待世界,理解这个世界背后的运行规律。
我觉得一本书籍能达到这个阅读境界就很值得推荐了,因为这相当于是在帮助我们树立世界观、沉淀方法论。所以今天的加餐环节,我想给你分享两本打动我的好书,或者说是以其为代表的两类好书,跟你聊聊我读这些书的感受和收获,希望也能给你一些启迪。
那第一本书呢,就是《失控》。
《失控》
失控这本书的作者是《连线》杂志的主编凯文 · 凯利于1994年出版。这本书被很多人推崇据说张小龙就曾说过谁能看懂《失控》这本书谁就可以到他那里工作。
这本书的神奇之处在于它虽然成书于上个世纪90年代初但准确预测了后来互联网时代的一系列的创新更厉害的是它揭示了互联网时代背后蕴藏的道理。就如这本书的副标题所说的它揭示了“全人类的最终命运和结局”。
我自己呢,是在读过这本书后,对其中的内容感觉极为惊讶。我甚至怀疑,凯文 · 凯利到底是何方神圣,为何他能够写出这样的惊世之作。
我就拿《失控》中第二章的内容,跟你一起分享一下,做一次管中窥豹。
第二章的标题是“蜂群思维”。蜜蜂是一种社会性昆虫它们总是一大群一起生活。在研究蜂群的时候你会发现一群蜜蜂相当于构成了一个单独的生命体这个生命体比单只的蜜蜂更加高级。举个例子单只蜜蜂只有6天的记忆力而一个蜂群却拥有三个月的记忆时间这是个体记忆与群体记忆的区别之处。另外这个生命体会比单只蜜蜂拥有更长的寿命且具有繁殖能力能分化出新的蜂群。
这样看起来它似乎符合一个生命体的所有特征。而这种把很多个体连接起来构成一个更高级的存在的现象就叫做涌现Emergence
另一个能很好地解释涌现的例子,就是人类的大脑。大脑中的神经元,实际上就是一个很简单的个体,它们只知道在接收到信号的时候,对其他神经元发送信号。而基于很多亿的神经元所涌现出来的大脑,却具备记忆、推理、情感等很高级的能力。试想,如果你是一个神经元,你其实是根本无法理解,以亿万个你构成的一个超级生命体,竟然会拥有情感、逻辑推理这种东西。因为在一个神经元的世界里,它只有接收信号和发送信号这两件事情。
你再往下思考,就会发现人类社会正是由亿万个你我构成的,那人类社会是不是一个超级生命体呢?这个生命体在思考什么,我们作为一个神经元级别的存在,如何能理解呢?或者说,思考仅仅是我们这个级别的个体所能理解的事情。而这个超级生命体所做的事情,可能已经根本不是人类的思考这种层面的事情了呢?早期人类的宗教,以及宗教中的神,也是高于单个的人类个体的。那么,它们是不是这个超级生命体在人类历史中早期的一种呈现方式呢?
我们再来假设一下,当前的互联网时代,连接网络的计算机、各种智能手机、智能设备越来越多,甚至已经开始接近大脑神经元的数量了。那么,它们会不会涌现出一个超级生命体?这个生命体是否会具备自己难以撼动的意志,而我们必须屈服于这种意志呢?
怎么样?这本书里的观点,是否也能同样给你带来启发,开一个大大的脑洞?是不是也引起了你去一读的兴趣呢?
这个级别的内容,在《失控》里还有很多。再举一个例子:活系统的特征是“摇摇晃晃的平衡”,而处于稳定的系统就进入了死寂。从这个角度看,如果我们的生活中问题不断,其实正是活系统的特征,因为我们要谋求持续的不均衡,这样我们才有机会去改变它,这总好过稳定的、死寂的生活。你看,这样的结论都已经带有了哲学的特征,让我们在面对生活中的挫折时,会采取更加积极的心态。
于是,还是回到我前头提到的那个疑惑:为什么凯文 · 凯利会有这么深刻的洞察力,远远超越我们这些普通人呢?
经过研究,我发现原来书中的很多观点,其实是对从上个世纪中叶以来,各学科的科研成果的总结,然后通过一个资深科技编辑的叙述普及给大众。看到这里,我才放心了:原来并不是出了一个多么逆天的天才,而是我自己对科技发展的新成果,以及其中蕴含的新思想缺少了解。这些思想或理论呢,包括了很多同学都知道的系统论、控制论和信息论三大论,以及后来的协同学、博弈论、突变论、混沌理论、分型理论、耗散结构和复杂性理论,等等。它们在过去的几十年间不断地发展,并正在形成一个宏大的、崭新的世界观和方法论体系。现在的一个新兴学科——复杂科学,似乎就是这些元素的集大成者。
我以前对复杂科学了解得不多,但我觉得其实也不能怪我。因为我们在中学、大学学的那些知识,大部分都是用来描述简单系统的。比如在大多数情况下,天体的运行就是一个简单系统,我们用相对论这样的理论就能准确地预测天体的行为。
而复杂系统,其构成部分之间的相互作用关系比较复杂,难以预测。我还是拿天体来说,三颗星体的相互作用,就变得难以预测了,这就是著名的三体现象,也是刘慈欣小说名称的来源。蝴蝶效应、混沌系统,等等,说的也是这个现象。
可以说,复杂系统破除了对还原论的迷信。也就是,即使你把一个系统分割成一个个的“零件”,并且完全理解了每个“零件”,你也可能无法理解整体系统。因为整体不等于部分的简单相加,比如说,就算你理解了一个社会的经济体中的每个企业和消费者的行为,你也无法准确掌控宏观经济。
可是,了解这些,对你我有什么意义呢?
我先讲一个小的用途。作为软件架构师,你其实应该知道,当一个软件系统复杂到一定程度的时候,你要把它看成一个动态演化的有机体。你对系统做的任何改动,都可能会引起一些你完全预料不到的结果。这就是为什么,你可以花一点儿钱甚至是免费就能搭建一套简单的电商系统,但是像淘宝这样的大型系统,则需要几千人来建设和维护它。
再举个例子。我们现在都非常熟悉的微服务架构它的理念是一个大型软件系统是从一个个分布式的、自治的单元中涌现出来的。流媒体巨头NetFlix他们也是微服务架构的首批推动者之一。在NetFlix软件工程师们会设计一些叫做Monkey的程序随机地杀死一些服务看看系统能否正常地自动修复。发现了吗像微服务这样的复杂系统它的冗余和自愈的能力已经像是一个生命体了即使出现了一些突发的故障比如某些服务的宕机它也不会一下子全部瘫痪。
除了软件领域,与人类社会密切相关的系统,包括天气系统、生态系统、经济系统、社会系统,甚至包括人体本身,它们也都是复杂系统,所以现在的很多学科都在采用复杂系统的思维做研究。比如,采用演化的思维做心理学的研究,就形成了进化心理学的分支(其实更恰当的翻译方法是演化心理学,因为演化是没有方向性的)。这个学科的基本逻辑,就是现在人类具有的某种心理特质(比如为什么恋爱中男人更主动,女人更矜持),都是在进化中形成的。因为凡是没有这种心理特质的人类,都已经在进化过程中被淘汰了。
再进一步,其实你根据上面的信息可以得出一个结论:原来文科生研究的对象都是复杂系统。你一旦意识到这一点,你就可以通过复杂系统的研究成果,去研究原来属于文科生的研究范畴,比如说社会学、经济学、文学和哲学,从而拥有方法论上的优势。
给你简单举个例子,经济学中的宏观经济学部分,就是针对复杂系统的。这也是为什么大家总是说经济学家都是事后诸葛亮的原因:复杂系统是很难被简单地驾驭的。
甚至,你也可以用复杂科学的视角来重新审视哲学,特别是一些古代的哲学思维。因为基本上这些古老的哲学思想都是复杂系统的描述,是让你能够更好地适应自然系统和人类社会这两个复杂系统的一些解。比如说,儒家的思想,是理顺人际间的互动关系,从而缔造一个稳定的社会系统;而道家的思想,则是描述了包含人类社会和自然界的一个更大的系统规律。
有意思的是,凯文 · 凯利在《失控》的最后一讲,总结了复杂系统的特征,有很多地方跟道家的思想非常契合。比如说,“世界是从无中创造出来的”“从无数神经元的连接中,涌现出了大脑;而分子层面的连接,则涌现出了细胞”。
可以说,从《失控》这本书开始,就引起了我对复杂科学的兴趣,这个主题下的其他书籍,比如《复杂》,也非常值得你一读。
好,接下来,我再给你分享另一类好书,是关于文化的。而且它跟复杂科学这个主题,也是存在联系的。
文化与地方志
我从大学起,就对“文化”这个主题非常感兴趣,跟东西方文化有关的东西我都乱看了一气。大学时我读过一本书,是房龙的《人类的故事》,非常喜欢,因为它不但描述了历史事实,还描述了推动历史发展背后的原因和规律。我当时想,如果历史都这么写,那么大家学历史的时候肯定不会觉得枯燥。
因为我的思维特点是非常理科生的,我很难记住那些相互之间没有逻辑关系的事实,我也很难接受强加过来的一套体系,除非我能弄清楚它背后的逻辑。而如果一本书,它能讲清楚事实背后的因果关系的脉络,就比较令人愉悦了。
而我前面所说的复杂系统的一些研究成果,就可以用来理解这些文化背后的逻辑规律。我挺喜欢的一个独立学者王东岳,他写了一本书叫做《物演通论》。王东岳很喜欢解读东西方文化背后的脉络,看他写的书就让人有一下子把厚厚的书读薄的感受,非常过瘾。
不过我想,如果我没有读过《失控》及其相关理论,我可能又会对王东岳此人惊为天人,对其著作惊为天书。但在有了前面的知识积累以后,我就不会那么惊讶了。因为王东岳先生的思考,也是建立在大半个世纪以来的科研成果的基础上的。物演的“演”字,就是演化思维的体现。当然,他能够进行提炼和再创造,构造一个完整的知识体系,也相当值得敬佩。
其实说了这么多,我的意思是,文化可以用复杂科学的思维来解构。这个方法,特别适合像我这样的、擅长逻辑思维的理科生们。每当你观察到一个文化现象,你都能解构出这背后的原因,岂不是很有意思呢?
作为一个北方人,我这几年大部分的时间都在厦门,对这里的闽南文化做了饶有兴趣的观察。去过厦门旅游的同学,应该都知道厦门的文艺气氛还挺浓厚的。那为什么厦门会有这种调调呢?还有,你在旅游的时候,应该会发现厦门的一种小吃,叫做沙茶面。那为什么沙茶面会在厦门文化中涌现出来呢?
这就需要结合闽南这个地方的地理、历史等各个要素及其互动关系来做分析。不过,我准备在课后的留言里,再分享我对这几个问题的看法。你有兴趣的话,也可以发表你的观点。
类似的文化方面的问题,还能提出很多来,比如:
为什么泉州会成为海上丝绸之路的起点?
为什么孔圣人出在山东,而历代出状元最多的省份,却都在南方?
中国有很多古镇,每个古镇在历史上肯定都是富甲一方的地方,那究竟是什么因素才促使它们兴盛起来的?
如果某个地方有一个地理标识产品,是某种柿子,你能推测出那里的地质特点吗?
……
去年的时候,我因为一个项目,翻阅了某县的县志,结果没想到我会对县志如此感兴趣,读得津津有味。我才发现,通过县志我能了解一个地方的地理、历史、经济、文化、重要人物等各种信息。通过这些信息,我基本上就能看到一个由很多要素相互作用构造出来的一个复杂系统,就能读懂当地各种文化的成因,这非常有意思。
中国的很多文化积淀很丰富。如果有机会能够一点一点地解读过去,那该多好。我估计我会一直保持阅读并解读地方志的兴趣。最近回老家,家人又给了我一本我们县在民国时代的县志。看着这些书籍,我有一种强烈的感觉:即使你是在这里生、这里长的,你也不一定真的了解本地的文化。
我为什么会推荐你去读地方志和其他讲解文化现象的书,读懂自己的本地文化呢?
第一层原因是呼应我在加餐2“学习技术的过程其实是训练心理素质的过程”中提倡你多学点“无用之学”的观点。哪怕只是让你的灵魂更有趣不是也挺好的吗
第二层原因,是我作为一个理科生的思维方式。把自己所处的社会系统理解清楚,能够透过现象看到后面的逻辑,不是很有意思吗?
第三层原因,如果你能够运用复杂科学的思维,来理解现在的社会系统,其实是有实际意义的。举个例子,如果你要撰写一个商业计划,或者想给一个企业写一套软件,这就需要你理解其当前的商业系统、理解一个企业组织具体是如何运行的。而你之前的这些阅读积累,就会成为你的底蕴,成为你的智慧源泉呀!
小结
今天这一讲,我推荐了两本书,或者说是两类书。一类书,是以《失控》为代表,讲述与复杂性相关的话题。另一类书,是以地方志为代表的文化载体。之所以给你推荐这两类书,是因为它们给我如何观察和理解这个世界开启了一扇窗户,并且给我这样一个严谨的理科生,提供了一条去打开文史哲的大门的独特的、有优势的途径,希望能对你有所启发。
思考一下
你有没有阅读过《失控》?你对复杂科学有什么了解?复杂科学在你的领域里有什么应用?
你对自己出生地的文化了解吗?你有没有曾经发现一个文化现象背后的逻辑脉络?你觉得多研究点文化现象对于自己的职业生涯有好处吗?
欢迎在留言区发表你的观点。如果今天的加餐让你有所启发,也欢迎把它分享给你的朋友。

View File

@ -0,0 +1,133 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
不定期加餐4 从身边的牛人身上,我学到的一些优秀品质
你好,我是宫文学。
今天的加餐环节,我想跟你分享一下让我很敬佩的那些牛人,以及从他们身上我所能学到的优秀品质。我希望你也能从这些人的故事上得到启发。这里为了叙述方便,我就不提具体的名字了。你只需要了解他的故事,从中有所感悟就好了。
我把这些牛人分为了两类,一类是搞技术的,一类是创业的。由于我自己也身兼两重身份,所以我很关注这两类人中能带给我启发的人。
首先来说说第一类人,搞技术的,也就是我们常说的极客们。
我所理解的极客
我曾经在技术圈子里参加过比较多的活动,特别是开源圈子的活动,因此也接触了不少技术大牛,国内国外的都有。
早在2000年的时候我就听过理查德·斯托曼Richard Stallman的讲座听他布道自由软件。Stallman是GNU这个组织的创始人他也发起了GPL开源协议。更重要的是他是GCC编译器的主要作者所以跟我们这门课也是很有渊源的。记得当时他给我们放一个幻灯片的时候用的是Linux上的一个软件界面没有微软的PowerPoint那么酷炫。但你能想到Stallman是绝对不会用PowerPoint的。
后来在参加和组织开源技术活动的过程中,我也接触了不少国内国外的技术团队,他们在很多方面刷新了我的认知、拓宽了我的视野,也让我更理解极客都是一些什么样的人。
在我看来,这些人应该就是合格的极客。那么,怎样才能被称为极客?是技术水平高吗?我想不是的。技术水平高,其实只是一个结果。真正让极客显得与众不同的,其实是他们对待技术的态度,乃至是对待人生的态度。这些特质,也能给所有做技术的人一些启发。
首先,是热爱技术。
跟普通人只是把技术作为一个谋生的手段不同,极客们是真心喜欢技术,热衷于钻研和探讨各种技术细节。他们在对待工作的时候,绝不会把某项工作做到能交差就行,他们想要做到完美。
我之前公司的一位股东,他在做程序员的时候,曾经接到领导的一项任务,给了他一块语音板子,让他研究一下能否做呼叫中心的功能。两个星期以后,再问他,技术上是否可行?他说,已经做完了。不仅做完了,他还考虑了各种扩展性。比如,给他的板子只有八个语音口,但他写的程序考虑了用不同的板子,有不同的口的情况。以至于后来很多年,基于他的程序做的呼叫中心系统,底层都不用做很大的改动。
我这位朋友我在加餐2中也提到过。他因为对于底层软件的深刻理解力被挖到中间件公司做老总。后来又在创业什么的最近又写了一套科幻小说。不管什么时候我总能从他身上吸取到一些东西。
另一个例子,是我一个在苹果工作的同学提到的。这位同学负责苹果的文件系统的开发,我下面还会给你讲他的故事。这里是他讲的另一件事情。一次,一位博士分配到他们组,一时没有合适的工作给他干,就先让他做一阵测试。结果这位老兄,彻底升级了测试系统,对于大量的工作都实现了自动化测试,给整个团队带来了巨大的价值。
这个故事也让我更新了看待测试工作的视角。我现在基本上不会去招聘那些因为对自己的技术能力没有信心,而选择去做测试工作的人。我认为测试工作需要极大的技术热情才能做好。
我想,不管是从事什么岗位,能够热爱自己所做的事情,都是非常值得庆幸的。反过来,如果不喜欢自己所做的事情,为什么要去凑合呢?
换句话说能够做自己喜欢的事情其实是有所取舍、有所牺牲的。林纳斯·托瓦兹Linus Torvalds就喜欢领着一拨人折腾Linux。如果他去做某个大公司的CTO甚至是创业合伙人也无不可。但他选择的是自己喜欢的生活方式。他没有太去想自己因此损失了多少发财的机会。
这就涉及到了第二点,就是极客们洒脱的生活态度。
极客们所展现出来的这个特质,从某种意义上来看是更具魅力的。很多极客,都是不愿意以“生活所迫”为借口,选择自己不喜欢的工作和生活方式。
我在加餐1分享远程办公话题的时候就提到过一家芬兰公司。这家公司都是远程办公的其中有的员工是一边全球旅游一边工作的。这些技术型的公司正是以这种方式吸引那些真正的极客加入。
还有一次我参加一个技术活动我的朋友C指着一个老外说这家伙在泰国买了一个小岛自己住还弄了个度假村什么的。说实话这样的归园田居的生活方式对像我们这样浸染在中华文化中的人来说是有很大的吸引力的。但我们有多少人敢于不从众去选择自己喜欢的生活方式呢
我还有的朋友是依托自己的技术创业的。创业这件事当然很不容易,但他们通常都会保持积极乐观的态度,并没有因为自己的项目没有及时被社会认可,就变得垂头丧气。
那第三点,就是极客们看待这个世界的方式:用代码说话。
极客这群人是不大讲政治的。他们一般只认真实的本事。Linus就有一句名言“Talk is cheap, show me the code.”,这也代表了极客们的精神。一个人的水平如何,看看他写的代码,或者至少看看他发表的文章,其实差不多就有数了,这个是伪装不了的。
早在智能手机流行前有一次聚会我一个搞Linux的朋友F就拿出了一台手机里面装着Linux、图形界面、App什么的看着都还行。这都是他鼓捣出来的。其实再加把劲比如支持用Java开发应用这就是一个Android系统了。而Android的创始人安迪·鲁宾Andy Rubin差不多也是这样一个极客。前一阵我跟一个公司的老总聊天。他问我为什么中国搞不出安卓来我给他解释了原因。其实不是我们没有这样的技术在极客们的眼里最早的那个安卓版本也没什么大不了的。只不过我们没有掌握技术生态而已。
极客们一般对系统底层的技术都比较熟悉。像安卓系统这种看似很高大上的东西,不会让他们心生畏惧。这些人在互相交流的时候,也会谈论一些底层技术。几句话下来,心里已经有数。
然后呢?他们之间会缔结惺惺相惜的友谊。两个极客之间的交往可以极其简单,他们甚至不需要见过面,只需要见过对方的代码,或者读过对方的文章,就会认可彼此。如果有事情,直接打招呼就行。
某互联网大厂是如何把自己的底层技术搞扎实的呢据我了解就是找到一个开源圈的大牛。这位大牛进去了以后又给技术社区的其他人打招呼说这里有什么技术难题需要解决过来吧。于是就聚集了一个小组的牛人搞出了非常好的成绩。这就是极客们的特殊的社交方式他们知道who is who并且志同道合的人愿意聚到一起。如果光靠HR部门和猎头公司来做要想达成这样的结果是很难的。
Github在某种意义上也是把极客们的这种倾向给充分地引导了出来。它从一个代码托管工具几乎已经变成了程序员的社交网站。
这里我是想说明一个观点,那就是技术人并没有怀才不遇这一说。把真本事亮出来,所有的事情会变得简单很多。
好了,这就是我总结的极客们给我的三点印象:热爱技术、生活洒脱、凭本事说话。这些特质,都是我很欣赏的,也常常作为参照来调整自己。
比如说,我觉得自己也挺热爱技术的,但是在前些年,我觉得自己不够洒脱,做不好取舍,总是想各方面都兼顾,结果哪方面都顾不好。所以还不如在自己喜欢的事情上全情投入,不去计较太多得失,反倒会更加心情愉快,做事情的结果也更好。
你可能会问,那这些极客都发展得怎么样呢?
我所认识的极客,有的是在小公司工作,有的是在大公司工作,还有的是在创业。不过,不管从事什么岗位,似乎都发展得不错。我想,这是因为他们从底层上选择了一个更好的发展逻辑:首先是做好取舍,让自己能够专注技术;在拥有了比较好的技术底蕴以后,他们也有更好的施展自己才华的平台;在专注于技术价值的同时,他们的生活也变得简单和健康。
OK讲完了搞技术的我再讲讲搞创业的朋友的故事以及他们给我的启迪。
创业者这个物种
我周围的朋友有不少是搞创业的。这些人往往都有一些很特别的点,让我欣赏、赞叹乃至仰慕。
首先一点,是坚韧不拔的意志力。
我们都知道,创业肯定不是简单的事情。而让一个企业能够穿越惊涛骇浪,最重要的就是创始人坚韧不拔的意志品质。
我本科的同班同学中,就有一个创业者,他公司的主营业务是户外运动用品,目前已经做到上市了。他的性格就很坚韧,我给你讲两个故事。
第一个在他成为我北大的同学之前其实曾经考上了一所技术类的军校。但后来他发现自己并不喜欢那里于是就想退学。可是军校岂可以当作儿戏想来就来想走就走为了能够退学他想了很多办法包括自己注射大剂量的抗生素产生精神恍惚的效果以便让校医诊断为精神疾病此法不成又从3楼的阳台上滚下来想把胳膊摔断以此理由退学……后来校领导实在看他态度坚决也就同意了他退学。他又重新参加了高考选择了他喜欢的学校和专业。
第二个大约在2006年我们一些同学因为毕业十周年又聚到了一起去内蒙古草原上玩其中一项活动就是骑马。我的这位同学骑术很好在草原上策马狂奔。不过在一个地方突然马失前蹄他从马背上摔了下来。这真是很惊险的一个意外我们在场一群人看了都心惊肉跳。不过他休息了一会以后又要了一匹马上马继续策马狂奔。晚上我们问他为什么刚摔了又骑他说如果今天不骑以后就不敢骑了。
说到这我想再多讲一个例子。这是我同一级的另一个同学的故事他是社会学系的。如果我说他的名字很多同学应该都会知道。他从2000年开始做一个与汽车有关的网站结果后来互联网泡沫破裂然后投资人撤资。他就自己筹了2000万买下了投资人的股份坚持做了下去直到2011年上市。想想看那个年头的2000万是多大的压力。但他就是咬着牙挺过来了。
我不知道有多少人能拥有像他们这样钢铁般的意志力。并且,令我沮丧的是,我怀疑这种个性可能主要是天生的?反正我是万万难以做到的。所以,创业这件事情,其实不是每个人都适合去做。而我这两个同学能做到上市,也绝不是偶然。
不过,为了不让自己的希望完全破灭,我还是倾向于相信意志和勇气这样的事情,至少在部分上是可以后天磨炼的。我在大学的时候练习过拳击,因为我觉得拳击可以锻炼人的勇气。来拳的时候不能眨眼,是拳击运动员的基本素质。那么在创业中,如果我们每次都去积极地面对挑战,那面对困难的能力也会越来越强。
我认识的其他几个创业者,虽然不像这两位那么夸张,但在意志力方面,也都属于罕见的。比如,某个技术社区,其创始人能够做到天天更新内容,十年如一日,这就是常人所不及的。最近我通过写编译原理的课程,也对内容编写这件事有了一定的体会。这样的事情,做一个星期、一个月、一个季度,是凭着兴趣和热情就可以做到的。而长年累月地去做,你要说没有意志力的因素,那是不可能的。
说完了强大的意志力,我再来说说我钦佩的这些人的第二点品质,就是有主见,不从众。
我观察,这些创业成功的人,往往判断事情都有自己的标准,这些标准往往与大众是不一致的。
还是说我同班的那个同学。在学校读书的时候,他经常就会消失不见了,过一阵再重新出现,他告诉我们,这次去陕北了,有什么感受,怎样怎样。过一段时间又会消失,回来后,说自己在新疆沙漠里独自走了几天,遇到被人追赶,差点殒命,等等等等。
等到快期末考试的时候他拉着我在未名湖边给他补习高数说能及格就行。几年以后在创业的过程中他还读了个清华的MBA班也是连毕业证都没要。按他的意思来说就是学到知识就行了。证书什么的不重要。
而我们这些俗人,天天使劲读书。等到毕业以后,又根据习惯和潮流,很多人又去出国,虽然我敢说,大部分同学那时候都想不清楚出国到底要干嘛。
所以从某种意义上来讲,比尔·盖茨、马克·扎克伯格等人敢于辍学创业,本身就意味着他们不是一般人。
而作为对比,还有一些人,都不管自己什么年纪了还在花高价去混文凭,不停地想往自己身上贴一些标签,来为自己壮胆。我觉得,这些人不要说创业了,给他一个重要的职位都是一件很冒险的事情。
前面我也提到了有些极客,会基于自己的兴趣爱好来创业。他们喜欢的技术和产品,往往在很长的时间内都不会得到社会的认可,不能变成有经济价值的商品。然而他们就是会坚持自己的方向。这些人,也是我学习的榜样。
这些技术创业者有的发展比较顺利但似乎也不是刻意为之。比如上海的小X我跟他在技术活动上有几面之缘。他搞了一个用于物联网的小小的OS搞了很多年了前两年突然听说融了很多资估值不错。我觉得资本投在这些人身上是投对了。
也有的朋友,会经历一些坎坷。但是他们总是按照自己的方式去折腾,保持对科技发展趋势的敏锐观察。每隔一段时间,我总能从他们那里听到一些新的思想和动态。就拿我一个做移动端底层平台朋友来说,他做这个方向已经很多年了。我相信他肯定会做成。不过先不说未来结果如何,至少我觉得他的生活状态是洒脱的、阳光的、不纠结的。
小结
今天的加餐,我给你分享了周围搞技术的和做创业的一些朋友的故事。这些故事跟你有什么关联呢?
首先,你选择了编译原理这门课程,基本上已经说明你有成为一名极客的潜质,否则也不会给自己这个挑战。但是在这个过程中呢,你可能会遇到很多的困难和心理上的纠结。我希望通过我分享的故事,能够帮助你做好取舍,丢掉包袱,健康阳光地拥抱作为一个技术从业者的职业生涯。
而如果你不小心选择了创业这条路,我也希望你能够像故事中的人物一样,去磨炼自己的意志力,以及坚持自己的主见。成功不成功不敢保证,至少你的生活会是很有价值的,不会后悔的。
以上也是对我自己的勉励,希望能跟你共勉。如果你或你身边也有类似的故事,欢迎在留言区分享出来。同样,也非常欢迎你把这一讲分享出去。
感谢你的阅读,我们结束语见!

View File

@ -0,0 +1,266 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
划重点 7种编译器的核心概念与算法
你好,我是编辑王惠。
阶段性的总结复习和验证成果是非常重要的。所以在8月7日到8月12日这为期一周的期中复习时间里我们先来巩固一下“真实编译器解析篇”中的重点知识。你可以通过学习委员朱英达总结梳理的划重点内容以及涵盖了关键知识点的7张思维导图来回顾7种语言编译器的核心概念与算法。
另外宫老师还精心策划了10道考试题让你能在行至半程之时做好自检及时发现知识漏洞到时候一起来挑战一下吧
在期中复习周的最后,我还会邀请一位优秀的同学来做一次学习分享。通过他的学习故事,你也可以借此对照一下自己的编译原理学习之路。
好,下面我们就一起来复习这些核心的编译原理概念与算法知识吧。
Java编译器javac
Java是一种广泛使用的计算机编程语言主要应用于企业级Web应用开发、大型分布式系统以及移动应用开发Android。到现在Java已经是一门非常成熟的语言了而且它也在不断进化、与时俱进泛型、函数式编程、模块化等特性陆续都增加了进来。与此同时Java的编译器和虚拟机中所采用的技术也比 20 年前发生了天翻地覆的变化。
Java的字节码编译器javac是用Java编写的它实现了自举。启动Java编译器需要Java虚拟机默认是HotSpot虚拟机使用C++编写)作为宿主环境。
javac编译器的编译过程主要涉及到了这样一些关键概念和核心算法
词法分析阶段:基于有限自动机的理论实现。在处理标识符与关键字重叠的问题上,采用了先都作为标识符识别出来,然后再把其中的关键词挑出来的方式。
语法分析阶段使用了自顶向下的递归下降算法、LL(k)方式以及多Token预读处理左递归问题时采用了标准的改写文法的方法处理二元表达式时采用了自底向上的运算符优先级解析器。
语义分析阶段:会分为多个小的阶段,且并不是顺序执行的,而是各阶段交织在一起。
语义分析阶段主要包含ENTER建立符号表、PROCESS处理注解、ATTR属性分析、FLOW数据流分析、TRANSTYPES处理泛型、TRANSPATTERNS处理模式匹配、UNLAMBDA处理 Lambda和 LOWER处理其他所有的语法糖比如内部类、foreach 循环等、GENERATE 阶段生成字节码等。在ATTR和FLOW这两个阶段编译器完成了主要的语义检查工作。
注意:生成字节码是一个比较机械的过程,编译器只需要对 AST 进行深度优先的遍历即可。在这个过程中会用到前几个阶段形成的属性信息,特别是类型信息。
参考资料:
关于注解的官方教程,参考这个链接。
关于数据流分析的理论性内容参考龙书Compilers Principles, Techniques and Tools第二版的9.2和9.3节。也可以参考《编译原理之美》 的第27、28讲那里进行了比较直观的讲述。
关于半格这个数学工具可以参考龙书第二版的9.3.1部分也可以参考《编译原理之美》的第28讲。
Java语言规范第六章参考Java虚拟机指令集。
Java JIT编译器Graal
对于编译目标为机器码的Java后端的编译器来说主要可以分AOT和JIT两类如果是在运行前一次性生成就叫做提前编译AOT如果是在运行时按需生成机器码就叫做即时编译JIT。Java以及基于JVM的语言都受益于JVM的JIT编译器。
在JDK的源代码中你能找到src/hotspot目录这是 JVM 的运行时HotSpot虚拟机它是用C++编写的其中就包括JIT编译器。
Graal是Oracle公司推出的一个完全用Java语言编写的JIT编译器。Graal编译器有两个特点内存安全相比C++实现的Java JIT编译器而言与Java配套的各种工具比如ID更友好、更丰富。
Java JIT编译器的编译过程主要涉及到了这样一些关键概念和核心算法
分层编译C0解释器、C1客户端编译器、C2服务端编译器。不同阶段的代码优化激进的程度不同且存在C2降级回C1的逆优化。
IR采用了“节点之海Sea of Nodes整合了控制流图与数据流图符合 SSA 格式,有利于优化算法的编写和维护。
两个重要的优化算法:内联优化和逃逸分析。
几个重要的数据结构HIR硬件无关的IR、LIR硬件相关的IR、CFG控制流图
寄存器分配算法LinearScan。
金句摘录:“编译器开发的真正的工作量,都在中后端。”
参考资料:
GraalVM项目的官方网站Graal的Github地址Graal项目的出版物。
基于图的IR的必读论文程序依赖图-J. Ferrante, K. J. Ottenstein, and J. D. Warren. The program dependence graph and its use in optimization. July 1987Click的论文-A Simple Graph-Based Intermediate Representation介绍Graal IR的论文-Graal IR: An Extensible Declarative Intermediate Representation。
关于优化算法:多态内联-Inlining of Virtual Methods逃逸分析-Escape Analysis for Java部分逃逸分析-Partial Escape Analysis and Scalar Replacement for Java。
Python编译器CPython
Python诞生于上个世纪90年代初作者是荷兰计算机程序员吉多·范罗苏姆Guido van Rossum。Python语言的特点是自身语法简单容易掌握强调一件事情只能用一种方法去做具备丰富的现代语言特性如OOP、FP等其实现机制决定了易于集成C++扩展不仅便于利用一些已有的、经典开源的高性能的C/C++库同时也可以很方便地编写自己的C++扩展,实现一些高性能模块。
另外Python使用了pgen这样的生成编译器的工具。pgen能够基于语法规则生成解析表Parse Table供语法分析程序使用。你可以通过修改规则文件来修改Python语言的语法pgen能给你生成新的语法解析器。它是把EBNF转化成一个NFA然后再把这个NFA转换成DFA。基于这个DFA在读取Token的时候编译器就知道如何做状态迁移并生成解析树。Python用的是 LL(1) 算法。
CPython编译器编译器的编译过程主要涉及到了这样一些关键概念和核心算法
语法分析首先是生成CSTConcret Syntax Tree具体语法树接着生成AST抽象语法树。CST的特点是完全基于源程序结构构建出树结构它比AST啰嗦但更精确地反映了语法推导的过程。而AST的特点是去掉了树结构上繁杂冗余的树枝节点更简明扼要更准确地表达了程序的结构。
语义分析Python通过一个建立符号表的过程来做相关的语义分析包括做引用消解等。Python语言使用变量的特点是不存在变量声明每个变量都是赋值即声明因此在给一个变量赋值时需要检查作用域确认当前操作是给全局的变量赋值还是在局部给声明新变量。
生成字节码:这个工作实际上包含了生成 CFG、为每个基本块生成指令以及把指令汇编成字节码并生成 PyCodeObject 对象的过程。另外,生成的字节码在最后放到解释器里执行之前,编译器还会再做一步窥孔优化工作。
运行时机制Python的运行时设计的核心就是PyObject对象Python对象所有的特性都是从PyObject的设计中延伸出来的。其虚拟机采用了栈机的架构。
参考资料:
python.org网站下载3.8.1版本的源代码。
GDB的安装和配置参考这篇文章。
Python的开发者指南网站。
pgen的工具程序Parser/pgen。-
由于CPython最新的Master分支上的代码调整此处pgen的链接地址调整为CPython3.9版本分支上的pgen相关代码。
Python的字节码的说明。
Python的内置类型。
JavaScript编译器V8
V8是谷歌公司在2008年推出的一款JavaScript编译器主要由C++编写而成。V8主要应用于Chrome浏览器后来也被开源社区中诸如Node.js等项目所使用。其最为突出的特点就是“快”由于JavaScript是在浏览器下载完页面后马上编译并执行它对编译速度有更高的要求。因此V8采用了一系列技术手段优化编译和启动阶段运行速度。
在设计上V8结合了分阶段懒解析、空间换时间等设计思路突出了解析、启动阶段运行的时间开销。
对源代码的Parse进行了流Stream化处理也就是边下载边解析。
预解析PreParse处理也就是所谓懒解析最核心的设计思想每个JS函数只有被执行的时候才会解析函数体的内部逻辑。
另外V8的很多地方体现出了与Java编译器异曲同工之处。比如它将JavaScript源代码的编译分为了由Ignition字节码解释执行和TurboFan的JIT编译机器代码执行两部分组成类似于Java编译器的字节码解释执行和Graal优化编译后执行两阶段TurboFan编译器的IR也采用了Sea of Nodes这一点类似于Java的Graal编译器且也涉及到了内联优化和逃逸分析算法。
其运行方式分为两类:
常规情况下Ignition字节码解释执行
编译器判定热点代码TurboFan JIT编译成机器码执行并且TurboFan会依赖一些Ignition解释执行过程中的运行时数据来进行进一步优化使机器码尽可能高效。
因为JavaScript是动态类型语言因此对函数参数类型的推断以及针对性优化是一个V8的核心技术。V8涉及到的其他优化算法有
隐藏类Hidden Class。相同“形状”的JS对象会被以同一个隐藏类维护其数据结构。
内联缓存Inline Caching。针对隐藏类查找属性值时的指针偏移量进行内联缓存这属于结合隐藏类技术做进一步性能的优化。
参考资料:
V8项目的官网以及V8的源代码-官方文档。
了解V8的解析器为什么速度非常快Blazingly fast parsing, part 1: optimizing the scannerBlazingly fast parsing, part 2: lazy parsing。
了解Ignition的设计Ignition Design Doc宫老师在Github上也放了一个拷贝。
了解Ignition的字节码Understanding V8s bytecode。
V8的指针压缩技术Pointer Compression in V8。
介绍V8基于推理的优化机制An Introduction to Speculative Optimization in V8。
关于Ignition字节码做优化的论文Register equivalence optimization宫老师在Github上也放了一份拷贝。
Julia的编译器
Julia语言最初发行于2012年其最初是为了满足高性能数值分析和计算科学的需要而设计的。Julia同时兼具了静态编译型和动态解释型语言的优点一方面它的性能很高可以跟Java和C语言媲美另一方面它又是动态类型的编写程序时不需要指定类型。
Julia编译器的特点是
作为动态类型语言却能拥有静态类型语言的执行速度最关键的原因是使用了LLVM作为编译器后端针对动态类型的变量在运行时由编译器JIT编译生成多个版本的目标代码保证程序的高性能
由C、C++、Lisp和Julia四种语言开发而成编译器前端主要采用Lisp实现
考虑到对函数式编程的支持有别于面向对象编程的“单一分派”Julia的编译器提供了“多重分派”的功能。
Julia编译器的编译过程主要涉及到了这样一些关键概念和核心算法
Julia的编译器也是采用了递归下降算法来实现语法分析。
其内部IR采用了SSA格式主要作用是完成类型推断和内联优化。
Julia的IR会被转化成LLVM的IR从而进一步利用LLVM的功能。在转换过程中会用Julia的内建函数这些内建函数代表了Julia语言中抽象度比较高的运算功能。
参考资料:
LLVM的官网以及LLVM的源代码。
Julia的开发者文档中有对如何使用LLVM的介绍Working with LLVM。
对LLVM中的各种Pass的介绍LLVMs Analysis and Transform Passes。
《编译原理之美》的第25讲和第26讲宫老师对LLVM后端及其命令行工具做了介绍并且还手工调用LLVM的API示范了针对不同的语法结构比如if结构应该如何生成LLVM IR最后即时编译并运行。你可以参考一下。
Go语言编译器gc
Go语言是Google开发的一种静态强类型、编译型、并发型并具有垃圾回收功能的编程语言又名Golang。Go广泛应用于Google的产品以及许多其他组织和开源项目其创建的初衷就是主要面向于部署于大量服务器之间的分布式程序也就是我们今天所说的“云”。因此Go的主要优势聚焦于服务端高并发场景。
Go语言编译器的特点是
gc编译器除了少量标准库的内容是用汇编写的以外其绝大部分逻辑都是用Go语言本身写的因此实现了较为完整的自举Bootstraping从前端到后端的整个流程都使用Go语言实现在编程语言中是较为罕见的
教科书级别的设计,源代码完全自举、代码干净利索,因此非常适合作为学习参考。
Go语言编译器的编译过程主要涉及到了这样一些关键概念和核心算法
编译器前端gc编译器的词法分析和语法分析使用的都是非常经典、传统的算法如手写的递归下降算法、处理二元表达式时使用操作符优先级算法。
中间代码阶段SSA格式的IR基于CFG的IR利于“死代码”的发现与删除多遍Pass的优化框架。
机器码生成阶段线性扫描算法官方的gc编译器并没做指令重排这是基于编译过程中时间开销的考虑。
参考资料:
介绍gc编译器的主要结构Introduction to the Go compiler官方文档。
介绍gc编译器的SSAIntroduction to the Go compilers SSA backend官方文档。
Go compiler internals: adding a new statement to Go - Part 1、Part2。在这两篇博客里作者做了一个实验如果往Go里面增加一条新的语法规则需要做哪些事情。我们能够很好地、贯穿性地了解一个编译器的方法。
介绍gc编译器的SSA优化规则描述语言的细节Go compiler: SSA optimization rules description language。
介绍Go汇编的细节A Primer on Go Assembly和A Quick Guide to Gos Assembler。gc编译器采用的汇编语言是它自己的一种格式是“伪汇编”。
MySQL的编译器
MySQL是一个开放源码的关系数据库管理系统原开发者为瑞典的MySQL AB公司后几经辗转目前归属于Oracle旗下产品。在过去MySQL性能高、成本低、可靠性好因此成为了最流行的开源数据库。SQL可以称得上是最成功的DSL特定领域语言之一MySQL中的SQL解析模块则是这门DSL的非常具有可参考性的一个实现。MySQL使用C++编写有少量几个代码文件是用C语言编写的。
MySQL的编译器的特点是
SQL作为DSL中最具有代表性的一种语言学习它的编译器的实现可以为我们自己设计面向业务的DSL提供参考。
手写的词法分析、用工具bisonGNU的yacc生成的语法分析。
基于LALR处理二元表达式。
中后端优化方面MySQL是解释执行并没有做太多的机器相关优化在机器无关优化方面除了一些编译领域传统的优化技术之外还做了一些针对数据库特定场景的优化方式。
MySQL的编译器的编译过程主要涉及到了这样一些关键概念和核心算法
词法分析和语法分析
词法分析:手写的词法分析器。
语法分析由bison生成。bison是一种基于EBNF生成语法分析程序的工具可视为GNU版的yacc。
bison支持的语法分析算法LALR算法。
语义分析
MySQL中一些重要的数据结构THD线程对象、Table_ident对象。
上下文处理基于contextualize的上下文处理。
基于数据库“业务逻辑”的引用消解:库名、表名、列名、入口、查找和作用域(子查询)
机器无关优化
典型的优化:常量传播、死代码消除、常量折叠。
针对数据库场景的优化产生执行计划、生成JOIN对象。
机器相关优化
MySQL实际上对表达式是解释执行所以并没有真正意义上做任何机器相关优化。
列举了PG另一种类似MySQL的DB有通过LLVM的JIT优化。
参考资料:
下载MySQL的源代码跟踪MySQL的执行过程要用Debug模式编译MySQL具体步骤可以参考这篇开发者文档。
MySQL的内行手册MySQL Internals Manual。它能给我们提供一些重要的信息但文档内容经常跟源代码的版本不同步比如介绍源代码的目录结构的信息就过时了。需要注意一下。
bison的手册。
如果要加深对MySQL内部机制的了解宫老师推荐了两本书OReilly的《Understanding MySQL Internals》以及《Expert MySQL》。

View File

@ -0,0 +1,131 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
期末答疑与总结 再次审视学习编译原理的作用
你好,我是宫文学。到这里,咱们这门课程的主要内容就要结束了。有的同学在学习课程的过程中呢,提出了他感兴趣的一些话题,而我自己也会有一些想讲的话题,这个我也会在后面,以加餐等方式再做一些补充。接下来,我还会给你出一套期末测试题,帮你检测自己在整个学习过程中的所学所得。
那么,在今天这一讲,我们就来做个期末答疑与总结。在这里,我挑选了同学们提出的几个有代表性的问题,给你解答一下,帮助你更好地了解和掌握本课程的知识内容。
问题1学习了编译原理对于我学习算法有什么帮助
@无缘消受人间富贵:老师,想通过编译器学算法,单独学算法总是不知道有什么意义,每次都放弃,老师有什么建议吗?但是看到评论说用到的都是简单的数据结构,编译器用不到复杂的数据结构和算法?
针对这位同学提出的问题,我想谈一谈我对算法学习的感受。
前一阵,我在跟同事聊天时,提到了一个观点。我说,大部分的程序员,其实从来都没写过一个像样的算法。他们写的程序,都是把业务逻辑简单地翻译成代码。那么,如果一个公司写出来的软件全是这样的代码,就没有什么技术壁垒了,很容易被复制。
反之,一些优秀的软件,往往都是有几个核心的算法的。比如,对于项目管理软件,那么网络优化算法就很关键;对于字处理软件,那么字体渲染算法就很关键,当年方正的激光照排系统,就是以此为基础的;对于电子表格软件,公式功能和自动计算的算法很关键;对于视频会议系统,也必须掌握与音视频有关的核心算法。这样,因为有了算法的技术壁垒,很多软件就算是摆在你的面前,你也很难克隆它。
所以说,作为一名软件工程师,你就必须要有一定的算法素养,这样才能去挑战那些有难度的软件功能。而作为一个软件公司,其实要看看自己在算法上有多少积淀,这样才能构筑自己的技术壁垒。
那么,编译原理对于提升你的算法素养,能带来什么帮助呢?我给你梳理一下。
编译原理之所以硬核,也是因为它涉及了很多的算法。
在编译器前端主要涉及到的算法有3个
有限自动机构造算法这是在讲词法分析时提到的。这个算法可以根据正则文法自动生成有限自动机。它是正则表达式工具的基础也是像grep等强大的Linux命令能够对字符串进行模式识别的核心技术。
LL算法这是在讲自顶向下的语法分析时涉及的。根据上下文无关文法LL算法能够自动生成自顶向下的语法分析器中间还涉及对First和Follow集合的计算。
LR算法这是在讲自底向上的语法分析时涉及的。根据上下文无关文法LR算法能自动生成自底向上的语法分析器中间还涉及到有限自动机的构造算法。
总的来说,编译器前端的算法都是判断某个文本是否符合某个文法规则,它对于各种文本处理工作都很有效。有些同学也在留言区里分享,他在做全文检索系统时就会用到上述算法,使得搜索引擎能更容易地理解用户的搜索请求。
在编译器后端主要涉及到的算法也有3个
指令选择算法;
寄存器分配算法;
指令重排序(指令调度)算法。
这三个算法也有共同点它们都是寻找较优解或最优解而且它们都是NP CompleteNP完全的。简单地说就是这类问题能够很容易验证一个解对不对多项式时间内但求解过程的效率却可能很低。对这类问题会采用各种方法求解。在讲解指令选择算法时我介绍了贪婪策略和动态规划这两种不同的求解思路而寄存器选择算法的图染色算法则采用了一种启发式算法这些都是求解NP完全问题的具体实践。
在日常工作中,我们其实也会有很多需要求较优解或最优解的需求。比如,在文本编辑软件中,需要把一个段落的文字分成多行。而如何分行,就需要用到一个这样的算法。再比如,当做一个报表软件,并且需要分页打印的时候,如何分页也是同类型的问题。
其他类似的需求还有很多。如果你没有求较优解或最优解的算法思路,对这样的问题就会束手无策。
而在编译器的中端部分涉及的算法数量就更多了但是由于这些算法都是对IR的各种分析和变换所以IR采用不同的数据结构的时候算法的实现也会不同。它不像前端和后端算法那样在不同的编译器里都具有很高的一致性。
不过IR基本上就是三种数据结构树结构、图结构和基于CFG的指令列表。所以这些算法会训练你处理树和图的能力比如你可以在树和图中发现一些模式以此对树和图进行变换等等。这在你日常的很多编程工作中也是非常重要的因为这两种数据结构是编程中最常使用的数据结构。
那么,总结起来,认真学好编译原理,一定会给你的算法素养带来不小的提升。
问题2现代编程语言这么多我们真的需要一门新语言吗
@蓝士钦前不久看到所谓的国产编程语言“木兰”被扒皮后发现是Python套层壳真的是很气愤。想要掌握编译原理设计一门自己的语言但同时又有点迷茫现代编程语言这么多真的需要再多一门新语言吗从人机交互的角度来看任何语言都是语法糖。
关于是否需要一门新语言的话题,我也想跟你聊聊我自己的看法,主要有三个方面。当然,你也可以在此过程中思考一下,看看有没有什么跟我不同的见解,欢迎与我交流讨论。
第一,编程语言其实比我们日常看到的要多,很多的细分领域都需要自己的语言。
我们平常了解的都是一些广泛流行的通用编程语言而进入到每个细分领域其实都需要各自领域的语言。比如SaaS的鼻祖Salesforce就设计了自己的Apex语言用于开发商业应用。华为的实验室在研发方舟编译器之前也曾经研发了一门语言Cm服务于DSP芯片的研发。
第二,中国技术生态的健康发展,都需要有自己的语言。
每当出现一个新的技术生态的时候总是有一门语言会成为这个技术生态的“脚本”服务于这个技术生态。比如C语言就是Unix系统的脚本语言JavaScript、Java、PHP等等本质上都是Web的脚本语言而Objective-C和Swift显然是苹果设备的脚本语言Android虽然一开始用了Java但最近也在转成Kotlin这样Google更容易掌控。
那么,从这个角度看,当中国逐步发展起自己的技术生态的时候,也一定会孕育出自己的语言。以移动计算生态而言,我们有全球最大的移动互联网用户群和最丰富的应用,手机的制造量也是全球最高的。而位于应用和硬件之间的应用开发平台,我们却没有话语权,这会使中国的移动互联网技术生态受到很大的掣肘。
我在第40讲也已经分析过了Android系统经过了很多年的演化但技术上仍然有明显的短板使得Android平台的使用体验始终赶不上苹果系统。为了弥补这些短板各个互联网公司都付出了很大的成本比如一些头部应用的核心功能采用了C/C++开发。
并且Android系统的编译器在支持新的硬件上也颇为保守和封闭让中国厂商难以参与。这也是华为之所以要做方舟编译器的另一个原因。因为华为现在自研的芯片越来越多要想充分发挥这些芯片的能力就必须要对编译器有更大的话语权。方舟编译器的问世也证明了我们其实是有技术能力的可以比国外的厂商做得更好。既然如此我们为什么要受别人的制约华为方舟编译器团队其实也很渴望在方舟编译器之后推出自己的语言。至于华为内部是否已经立项这就不太清楚了但我觉得这是顺理成章的事情。
另外除了在移动端的开发上会受到很多掣肘在云端其实也一样。比如说Java是被大量后端开发的工程师们所掌握的语言但现在Java是被Oracle掌控的。你现在使用Java的时候可能已经多多少少感受到了一种不愉快。先不说Java8之后的收费政策就说我们渴望的特性如协程、泛型中支持基础数据类型等一直没有被满足就会感觉不爽。
我在讲到协程的时候就指出Java语言目前支持协程其实是很别扭的一种状态它都是一些第三方的实现并没有官方的支持。而如果Java的技术生态是由我们主导可能就不是这样了。因为我国互联网的并发用户数如此之多我们对更好的并发特性其实是更关切的。到目前为止像微信团队解决高并发的问题是用C++加上自己开发的协程库才实现的。而对于很多没有如此强大的技术能力的公司来说,就只能凑合了。
第三,实现一款优秀的软件,一定会用到编译技术。
每一款软件当发展到极致的时候都会变得像一个开发平台。这也是《黑客与画家》的作者保罗·格雷厄姆Paul Graham表达的思维。他原来的意思是每个软件写到最后都会包含一个Lisp的变种。实际上他所要表达的意思就跟我说的一样。
我前一段时间在北京跟某公司的老总探讨一个优秀的行业应用软件。这个软件在上世纪90年代就被开发出来了也被我国广泛采用。一方面它是一个应用软件另一方面它本身也是一个开发平台。所以它可以经过定制满足不同行业的需求。
但是,我们国内的软件行业的情况是,在去客户那里实施的时候,几乎总是要修改源代码,否则就不能满足用户的个性化需求。
很多软件公司想去克隆一下我刚才说的那套软件,结果都放弃了。除了有对领域模型理解的困难以外,缺少把一个应用软件做成软件开发平台的能力,是其中很大的一个障碍。
实际上,目前在很多领域都是这样。国外的软件就是摆在那里,但中国的工程师就是做不出自己的来。而对于编译技术的掌握和运用,就是能够提升国内软件水平的重要途径。
我在开头跟同事交流的时候,也提出了软件工程师技术水平修养提升的几个境界。其中一个境界,就是要能够利用编译技术,做出在更大范围内具有通用性的软件。如果你能达到这个境界,那么也一定有更大的发展空间。
问题3如何判断某门语言是否适合利用LLVM作为后端
@ヾ(◍°∇°◍)ノ゙老师很多语言都声称使用LLVM提升性能但是在Lua领域好像一直是LuaJIT无法超越
这个问题涉及到了如何利用后端工具的问题,比较有代表性。
LLVM是一个通用的后端工具。在它诞生之初首先是用于支持C/C++语言的。所以一门语言在运行机制上越接近C/C++语言用LLVM来做后端就越合适。
比如Rust用LLVM就很成功因为Rust语言跟C/C++一样,它们的目标都是编写系统级的程序,支持各种丰富的基础数据类型,并且也都不需要有垃圾收集机制。
那么如果换成Python呢你应该记得Python不会对基础数据类型进行细粒度的控制不需要把整型区分成8位、16位、32位和64位的它的整型计算可以支持任意长度。这种语义就跟C/C++的相差比较远所以采用LLVM的收益相对就会小一些。
而对于JavaScript语言来说浏览器的应用场景要求了编译速度要尽量地快但在这方面LLVM并没有优势。像我们讲过的隐藏类Shapes和内联缓存Inline Caching这样的对JavaScript很重要的机制LLVM也帮不上忙。所以如果在项目时间比较紧张的情况下你可以暂时拿LLVM顶一顶Safari浏览器中的JavaScript引擎之前就这么干过。但是要想达到最好的效果你还是编写自己的后端更好一些。
那对于Lua语言其实你也可以用这个思路来分析一下是采用LLVM还是自己写后端会更好一些。不过由于Lua语言比较简单所以实现后端的工作量应该也相对较小。
小结
这一讲,我主要回答了几个比较宏观的问题,它们都涉及到了编译原理这门课的作用。
第一个问题,我是从提升算法素养的角度来展开介绍的。编译原理知识里面涉及了大量的算法,我把它总结成了三大类,每类都有自己的特点,希望能对你宏观把握它们有所帮助。
第二个问题,其实是这门课程的一条暗线。我并没有在课程里去情绪化地鼓吹,一定要有自己的编译器、自己的语言。我的方式其实是想做一点具体的事情,所以在第二个模块中,我带着你一起探究了现有语言的编译器都是怎么实现的,破除你对编译器的神秘感、距离感;在第三个模块,我们又一起探讨了一下实现一门语言中的那些关键技术点,比如垃圾收集、并行等,它们都是如何实现的。
在课程最后呢,我又带你了解了一下具有中国血统的方舟编译器。我想说的是,其实我们不但能做出编译器和语言来,而且可能会做得更好。虽然我们对方舟编译器的分析还没有做完,但通过分析它的技术思路,你应该或多或少地感受到了它的优秀。所以,针对“我们真的需要一门新语言吗”这个问题,我的回答是确定的。并且,即使你不去参与实现一门通用的语言,在实现自己领域的语言,以及把自己的软件做得更具通用性这点上,编译原理仍然能发挥巨大的作用,对你的职业生涯也会有切实的帮助。
好,请你继续给我留言吧,我们一起交流讨论。同时我也希望你能多多地分享,做一个知识的传播者。感谢你的阅读,我们下一讲再见。

View File

@ -0,0 +1,164 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
热点问题答疑 如何吃透7种真实的编译器
你好,我是宫文学。
到这里咱们就已经解析完7个编译器了。在这个过程中你可能也积累了不少问题。所以今天这一讲我就把其中有代表性的问题给你具体分析一下。这样能帮助你更好地掌握本课程的学习思路。
问题1如何真正吃透课程中讲到的7种编译器
在课程中,我们是从解析实际编译器入手的。而每一个真实的编译器里面都包含了大量的实战技术和知识点,所以你在学习的时候,很容易在某个点被卡住。那第一个问题,我想先给你解答一下,“真实编译器解析篇”这个模块的学习方法。
我们知道,学习知识最好能找到一个比较缓的坡,让自己可以慢慢爬上去,而不是一下子面对一面高墙。那么对于研究真实编译器,这个缓坡是什么呢?
我的建议是,你可以把掌握一个具体的编译器的目标,分解成四个级别的任务,逐步提高难度,直到最后吃透。
第一个级别,就是听一听文稿,看一看文稿中给出的示例程序和源代码的链接就可以了。
这个级别最重要的目标是什么?是掌握我给你梳理出来的这个编译器的技术主线,掌握一张地图,这样你就能有一个宏观且直观的把握,并且能增强你对编译原理的核心基础知识点的认知,就可以了。
小提示关于编译器的技术主线和知识地图你可以期待一下在期中复习周中即将发布的“划重点7种编译器的核心概念和算法”内容。
在这个基础上,如果你还想再进一步,那么就可以挑战第二级的任务。
第二个级别,是要动手做实验。
你可以运行一下我给出的那些使用编译器的命令,打印输出调试信息,或者使用一下课程中提到的图形化工具。
比如在Graal和V8编译器你可以通过修改命令行的参数观察生成的IR是什么样子。这样你就可以了解到什么情况下才会触发即时编译、什么时候才会触发内联优化、生成的汇编代码是什么样子的等等。
这样,通过动手做练习,你对这些编译器的认识就会更加具体,并且会有一种自己可以驾驭的感觉,赢得信心。
第三个级别,是看源代码,并跟踪源代码的运行过程,从而进入到编译器的内部,去解析一个编译器的真相。
完成这一级的任务对你动手能力的要求更高。你最容易遇到的问题是搭建一个调试环境。比如调试Graal编译器要采用远程调试的模式跟你调试一个普通应用还是不大一样的。而采用GDB、LLDB这样的工具对很多同学来说可能也是一个挑战。
而且你在编译源代码和调试的过程中也会遇到很多与配置有关的问题。比如我用GDB来调试Julia和MySQL的时候就发现最好是使用一个Linux虚拟机因为macOS对GDB的支持不够好。
不过,上述困难都不是说真的有多难,而是需要你的耐心。遇到问题就解决问题,最终搭建出一个你能驾驭的环境,这个过程也会大大提升你的动手实践能力。
环境搭建好了,在跟踪程序执行的过程中,一样要需要耐心。你可能要跟踪执行很多步,才能梳理出程序的执行脉络和实现思路。我在课程中建议的那些断点的位置和梳理出来程序的入口,可以给你提供一些帮助。
可以说,只要你能做好第三级的工作,终归是能吃透编译器的运行机制的。这个时候,你其实已经差不多进入了高手的行列。比如,在实际编程工作中,当遇到一个特别棘手的问题的时候,你可以跟踪到编译器、虚拟机的内部实现机制上去定位和解决问题。
而我前面说了,掌握一个具体的编译器的目标,是有四个级别的任务。那你可能要问,都能剖析源代码了,还要进一步挑战什么呢?
这第四个级别呢,就是把代码跟编译原理和算法结合起来,实现认识的升华。
在第三级,当你阅读和跟踪程序执行的时候,会遇到一个认知上的挑战。对于某些程序,你每行代码都能看懂,但为什么这么写,你其实不明白。
像编译器这样的软件,在解决每一个关键问题的时候,肯定都是有理论和算法支撑的。这跟我们平常写一些应用程序不大一样,这些应用程序很少会涉及到比较深入的原理和算法。
我举个例子在讲Java编译器中的语法分析器的时候我提到几点。第一它是用递归下降算法的第二它在避免左递归时采用了经典的文法改写的方法第三在处理二元表达式时采用了运算符优先级算法它是一种简单的LR算法。
我提到的这三点中的每一点,都是一个编译原理的知识点或算法。如果对这些理论没有具体的了解,那你看代码的时候就看不出门道来。类似的例子还有很多。
所以,如果你其实在编译原理的基础理论和算法上都有不错的素养的话,你会直接带着自己的假设去代码里进行印证,这样你就会发现每段程序,其实都是有一个算法去对应的,这样你就真的做到融会贯通了。
那如何才能达到第四级的境界,如何才能理论和实践兼修且互相联系呢?
第一,你要掌握“预备知识”模块中的编译原理核心基础知识和算法。
第二你要阅读相关的论文和设计文档。有一些论文是一些经典的、奠基性的论文。比如在讲Sea of Nodes类型的IR的时候我介绍了三篇重要的论文需要你去看。还有一些论文或设计文档是针对某个编译器的具体的技术点的这些论文对于你掌握该编译器的设计思路也很有帮助。
达到第四级的境界,你其实已经可以参与编译器的开发,并能成为该领域的技术专家了。针对某个具体的技术点加以研究和钻研,你也可以写出很有见地的论文。
当然,我不会要求每个同学都成为一个编译器的专家,因为这真的要投入大量的精力和实践。你可以根据自己的技术领域和发展规划,设定自己的目标。
我的建议是:
首先,每个同学肯定要完成第一级的目标。这一级目标的要求是能理解主线,有时候要多读几遍才行。
对于第二级目标我建议你针对2~3门你感兴趣的语言上手做一做实验。
对于第三级目标我希望你能够针对1门语言去做一下深入探索找一找跟踪调试一个编译器、甚至修改编译器的源代码的感觉。
对于第四级目标,我希望你能够针对那些常见的编译原理算法,比如前端的词法分析、语法分析,能够在编译器里找到并理解它们的实现。至于那些更加深入的算法,可以作为延伸任务。
总的来说呢,“真实编译器”这个模块的课程内容,为你的学习提供了开放式的各种可能性。
好,接下来,我就针对同学们的提问和课程的思考题,来做一下解析。
问题2多重分派是泛型实现吗
@d:“多重分派能够根据方法参数的类型,确定其分派到哪个实现。它的优点是容易让同一个操作,扩展到支持不同的数据类型。”宫老师,多重分派是泛型实现吗?
由于大多数同学目前使用的语言,采用的都是面向对象的编程范式,所以会比较熟悉像这样的一种函数或方法派发的方式:
Mammal mammal = new Cow(); //Cow是Mammal的一个子类
mammal.speak();
这是调用了mammal的一个方法speak。那这个speak方法具体指的是哪个实现呢根据面向对象的继承规则这个方法可以是在Cow上定义的。如果Cow本身没有定义就去它的父类中去逐级查找。所以speak()具体采用哪个实现是完全由mammal对象的类型来确定的。这就是单一分派。
我们认为mammal对象实际上是speak方法的第一个参数虽然在语法上它并没有出现在参数列表中。而Java的运行时机制也确实是这么实现的。你可以通过查看编译生成的字节码或汇编代码来验证一下。你如果在方法中使用“this”对象那么实际上访问的是方法的0号参数来获取对象的引用或地址。
在采用单一分派的情况下,对于二元(或者更多元)的运算的实现是比较别扭的,比如下面的整型和浮点型相加的方法,你需要在整型和浮点型的对象中,分别去实现整型加浮点型,以及浮点型加整型的计算:
Integer a = 2;
Float b = 3.1;
a.add(b); //采用整型对象的add方法。
b.add(a); //采用浮点型对象的add方法。
但如果再增加新的类型怎么办呢?那么所有原有的类都要去修改,以便支持新的加法运算吗?
多重分派的情况,就不是仅仅由第一个参数来确定函数的实现了,而是会依赖多个参数的组合。这就能很优雅地解决上述问题。在增加新的数据类型的时候,你只需要增加新的函数即可。
add(Integer a, Float b);
add(Float b, Integer a);
add(Integer a, MyType b); //支持新的类型
不过,这里又有一个问题出现了。如果对每种具体的类型,都去实现一个函数的话,那么实现的工作量也很大。这个时候,我们就可以用上泛型了,或者叫参数化类型。
通过泛型的机制,我们可以让相同的实现逻辑只书写一次。在第三个模块“现代语言设计篇”中,专门有一讲给你进一步展开泛型的实现机制,到时你可以去深入学习下。
问题3安全点是怎么回事为什么编译器生成的某些汇编代码我看不懂
@智昂张智恩震请问老师和JVM握手就是插入safepoint的过程吗具体的握手是在做什么
你在查看编译器生成的汇编代码的时候,经常会看到一些辅助性的代码逻辑。它们的作用不是要把你的代码翻译成汇编代码才生成的,而是要去实现一些运行时机制。
我举几个例子。
第一个例子是做逆优化。比如V8中把函数编译成机器码是基于对类型的推断。如果实际执行的时候编译器发现类型跟推断不符就要执行逆优化跳转到解释器去执行。这个时候你就会看到汇编代码里有一些指令是用于做逆优化功能的。
第二个例子是在并行中会遇到的抢占式调度问题。协程这种并发机制是应用级的并发。一个线程上会有多个协程在运行。但是如果其中一个协程的运行时间很长就会占据太多的计算资源让这个线程上的其他协程没有机会去运行。对于一些比较高级的协程调度器比如Go语言的调度器就能够把一个长时间运行的协程暂停下来让其他协程来运行。怎么实现这种调度呢那就要编译器在生成的代码里去插入一些逻辑配合调度器去做这种调度。
第三个例子是垃圾收集。根据编译器所采用的垃圾收集算法在进行垃圾收集时可能会做内存的拷贝把一个对象从一个地方拷贝到另一地方。这在某些情况下会导致程序出错。比如当你读一个Java对象的成员变量的值的时候生成的汇编代码会根据对象的地址加上一定的偏移量得到该成员变量的地址。但这个时候这个对象的地址被垃圾收集器改变了那么程序的逻辑就错了。所以在做垃圾回收的时候相关的线程一定要停在叫做“安全点safepoint”的地方在这些地方去修改对象的地址程序就不会出错。
@智昂张智恩震 同学提出的问题就针对垃圾收集这种场景的。在Java生成的汇编代码里程序要在安全点去跟运行时做一下互动握手。如果需要的话当前线程就会被垃圾收集器停下以便执行垃圾收集操作。
所以你看,只有了解了一门语言的运行时机制,才能懂得为什么要生成这样的代码。关于垃圾收集和并发机制,我也会在第三个模块中跟你去做进一步的探讨。
问题4SSA只允许给变量赋一次值循环中的变量是多次赋值的不是矛盾了吗
@qinsi关于思考题SSA只允许给变量赋一次值如果是循环的话就意味着要创建循环次数那么多的临时变量了
@qinsi 同学问的这个问题其实挺深入,也很有意思。
是这样的。我们在做编译的时候大部分时候是做静态的分析也就是仅仅基于程序从词法角度Lexically的定义不看它运行时的状态。注意我这里说的词法不是指词法分析的词法而是指程序文本中体现的“使用和定义”use-def关系、控制流等。词法作用域Lexical Scope中的词法也是同一个意思。
所以SSA中说的赋值实际上是对该变量或称作值做了一个定义体现了变量之间的“使用和定义”use-def关系也就是体现了变量之间的数据依赖或者说是数据流因此可以用来做数据流分析从而实现各种优化算法。
小结
这一讲的答疑,我首先帮你梳理了学习真实世界编译器的方法。一个真实的编译器里涉及的技术和知识点确实比较多,但有挑战就有应对方法。我给你梳理了四级的学习阶梯,你探索内容的多少,也可以根据自己的需求和兴趣来把握。按照这个学习路径,你既可以去做一些宏观的了解,也可以在某个具体点上去做深入,这是一个有弹性的学习体系。
另外,我也挑了几个有意思的问题做了解答,在解答中也对涉及的知识点做了延伸和扩展。其中一些知识点,我还会在第三个模块中做进一步的介绍,比如垃圾收集机制、并发机制,以及泛型等。等你学完第三个模块,再回头看实际编译器的时候,你的认知会再次迭代。
好,请你继续给我留言吧,我们一起交流讨论。同时我也希望你能多多地分享,做一个知识的传播者。感谢你的阅读,我们下一讲再见。

View File

@ -0,0 +1,105 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
用户故事 易昊程序员不止有Bug和加班还有诗和远方
你好,我是编辑王惠。在处理这门课的留言时,我注意到易昊同学一直在跟随着宫老师的脚步,学习和实践编译原理的相关知识,留言的内容十分有见地、提出的问题也能看出是经过了他深入的思考。同时,咱们这门课也具有很强的互动性,所以我邀请他来和我们分享一下他的心得体会。
Hi我是易昊目前在武汉做Android开发已经工作12年了。很高兴能在这里跟你分享关于我学习编译原理的一些心得体会。
为什么我要再学编译原理?
首先我想给你解释一下我为什么会起“程序员不止有Bug和加班还有诗和远方”这样一个标题呢
这是因为作为一名应用开发者我经常会觉得自己只是在和源源不断的Bug以及项目进度作斗争日常工作好像已经无法给我带来技术上的成就感了。但我能肯定的是我对于技术的情怀并没有消失。我也认为我不应该只满足于完成日常的普通开发任务而是应该去做点更有挑战性的事情来满足自己精神上的追求。
那么,我为什么会选择学习编译技术呢?
首要的原因,是这门课的内容不像编程语言、数据结构那样清晰直观。加上在大学时期,学校安排的课时较短,只有半个学期,自己又没有对它足够重视起来,导致这门课只学了个一知半解,从而造成自己对计算机底层工作原理没有掌握透彻,留下了遗憾。
《程序员的自我修养链接、装载与库》里有句话让我印象深刻“真正了不起的程序员对自己程序的每一个字节都了如指掌。”虽然说得有点夸张但一个优秀的程序员确实应该理解自己的程序为什么能在计算机上运行起来。而不是在写完代码后忐忑不安地看着IDE在进行编译等终于能运行起来时大喊一声“哇能编译运行了好神奇”。所以工作了几年之后我一直想找机会能够弥补一下大学时期的遗憾把编译知识学扎实。
另外,编译技术的巨大挑战性,也是我想重拾学习的重要原因之一。
你可能听过一个段子:程序员的三大浪漫,是自己实现编译器、操作系统和渲染引擎。
其实一开始,我并不理解为什么编译器会在这其中,我猜大概是因为特别困难吧。
我刚接触编程的时候觉得能学好C、Java这样的编程语言已经很不容易了更何况要自己去用编译器实现一门语言。
而且编译原理这门课中还有相当多比较深奥、晦涩的理论想要掌握起来非常困难它完全不像学习普通技术那样几行代码就能运行一个hello world。同时你光有理论又完全不够编译器中包含了很多巧妙的工程化处理。比如会用抽象语法树来表示整个代码的语法结构在函数调用时参数和返回值在栈帧中的内存地址顺序等。
所以,不得不说,编译器是一个非常复杂的工程,编译原理是一个非常不容易消化、掌握的基础技术,让人望而生畏。
但是一旦掌握了之后,可以说就打通了计算机技术的任督二脉,因为你已经掌握了计算机运行中相当底层部分的原理,那么再去看其他技术就不会有什么大的障碍了。
而且在工作中即使没有什么机会需要自己去创造语言或者写编译器编译原理对我也很有帮助。举个例子吧Android有很多动态化的技术像ReactNative、Weex都会使用JavaScript作为脚本语言来调用原生接口那么为了实现原生语言如Java、Objective-C和JavaScript的通信ReactNative和Weex都在框架中嵌入了JavaScriptCore这个组件因为JavaSciptCore包含了一个JavaScript的解释器可以在运行时执行JavaScript。
那么如果你也想在自己的项目中使用类似的动态化技术但又不愿意被ReactNative和Weex技术绑死就需要自己去剖析JavaScriptCore的工作原理甚至要去实现一个类似的脚本语言执行框架。
而真的要自己去下载JavaScriptCore的源码来看你就会发现如果没有一定的编译原理知识作为基础是很难看懂的。但是如果你具备了编译原理知识基础其实会发现这些源码里面也不外乎就是词法分析、语法分析、IR等这些都是编译原理中的常用概念和算法。
还有就是日常工作中会碰到的某些比较棘手的问题,如果你不理解编译技术,可能就无法找到出现问题的根源。
比如我早期的工作中在开发C++代码的时候经常会遇到链接时找不到符号的问题那个时候我对“符号是怎么产生的”并不理解所以遇到这类问题我就会在IDE或者代码里盲目地尝试修改。
后来重新学习了编译技术之后,我就理解了编译器在编译过程中会产生符号表,每个符号包含了符号的名称、类型、地址等信息。之所以出现这类问题,可能是依赖的静态库没有包含这些符号,或者是类型不正确,再或者是这些符号的地址无法被正确解析,所以我用相应的工具去检查一下静态库文件的符号表,一般就可以解决问题了。
你看编译技术总能帮我解决一些我之前挠破头都想不出解决方案的问题。到现在我工作了十几年以后就会有一个越来越强烈的感悟最重要的还是底层知识。以前我也曾经感慨过计算机技术发展之快各种技术层出不穷就拿Android技术来说各种什么插件化、组件化、动态化技术让人眼花缭乱要不就是今天谷歌在提倡用Kotlin明天又开始推Flutter了。
所以有一段时间我比较迷茫,“我究竟该学什么呢”,但我后来发现,虽然那些新技术层出不穷,但万变不离其宗,计算机核心的部分还是编译原理、操作系统那些底层技术。如果你对底层技术真正吃透了,再来看这些时髦的新技术,就不会感到那么神秘了,甚至是可以马上就弄明白它背后的技术原理。原理都搞懂了,那掌握起来也就非常快了。
我是怎么学习专栏的?
我开始意识到自己需要重新学习编译技术是在2015年当时为了能够在Android的原生代码里运行JavaScript脚本我依次尝试了WebView、JavaScriptCore、Rhino等方案觉得这种多语言混合开发的技术挺强大也许能够改变主流的应用开发模式于是就想继续研究这些框架是怎么工作的。但是我发现自己对编译原理知识掌握得不牢导致学习这些技术的时候有点无从下手。
那个时候还没有极客时间这样针对性较强的学习平台,我是自己买了些书,有权威的龙书和虎书,也有《两周自制脚本语言》这样的速成书籍,但总是不得要领。
像龙书、虎书这样的,主要花了大量的篇幅在讲理论。但面对工作和家庭的压力,又不允许我有那么大片的时间去学习理论,而且没有实践来练习的话,也很容易忘掉。
像速成书籍这样的,虽然实现了一个很简单的编译器,但它大量的篇幅是在讲代码的一些细节,原理又讲得太少,知其然而不知其所以然。后来我就没有继续深入地学下去了。
直到偶然地在极客时间上看到了《编译原理之美》这门课,我简单看了下目录之后,就立马买了下来,因为感觉非常实用,既对理论部分的重点内容有深入浅出地讲解,也有与实际问题的紧密联系。学完这门课后,我感觉有信心去尝试写一点东西了,正好临近春节,就计划着在春节期间自己写些代码来验证学习的成果。结果不成想遇到了疫情封城,因为没法复工,就索性在家自己照着龙书和课程中给出的思路,看看能否自己实现个编译器出来。
结果我花了快两个月的时间真的写出来了一个简单的编译器能够把类似C语言风格的代码编译成汇编执行。
回想那段时间,虽然疫情很让人焦虑,但当我全身心地投入到对编译技术的钻研时,可以暂时忘记疫情带来的困扰。通过写这个小项目,我算是对编译器的工作过程有了个切身的体会,还把多年未碰的汇编又重新拾起来投入使用,可以说是收获颇丰,疫情带给我的回忆也没有那么痛苦了,从另一个角度看,甚至还有一定的成就感。
这个简单的编译器项目完成了之后,就激发了我更大的兴趣。因为我毕竟只是实现了一个玩具型的编译器,那么工业界的编译器是如何工作的呢?
这个编译器我是用LL算法来实现的语法分析但龙书上还大篇幅地讲了LR、SLR、LALR那么实际中到底是使用LL算法还是LR算法呢
还有,我的编译器没有什么优化的功能,真实的编译器都有哪些优化措施呢?
这些问题吸引着我要去寻找答案。结果正巧,宫老师又推出了《编译原理实战课》,深入浅出地讲解各大编译器的工作原理,这可正好对我的胃口,于是又毫不犹豫地买下了。
上了几节课之后觉得收获很大特别是讲解javac和Graal编译器的部分。这两部分都给出了如何基于源码去剖析编译器的原理实际操作性很强我跟着宫老师给出的步骤下载和一步步地调试、跟踪源码印象十分深刻。特别是宫老师介绍的javac在处理运算符优先级时引入了LR算法从而避免了教科书上介绍的当使用LL方式时为了消除左递归要写多级Tail函数的问题这些都让我对编译技术的实际应用理解得更加深刻了。在学习Graal时我也是花了不少的时间去配置Windows环境包括下载安装Kali Linux、OpenJDK等才终于把Graal跑起来。通过这样的实际操作体验到了“折腾”的乐趣对动手能力也是一种锻炼。
除此之外,课程中还有丰富的流程图、类图和思维导图,因此我可以按图索骥,去研究自己感兴趣的知识点,不用再苦苦地从海量的源码中去大海捞针,学习效率得到了很大提升。
如何更好地学习编译原理?
通过这段时间的学习后,我发现,编译原理其实并没有想象中的那么困难。我觉得计算机技术的一个特点就是像在搭积木,有时候只是知识点多、层次关系复杂而已。
编译技术尤其如此从前端到后端从词法分析到语法分析到语义分析、IR最后到优化。这些地方包含着各种知识点但这些知识也不是凭空变出来的而是环环相扣、层层叠加出来的。因此你在学习编译技术时一定要静下心来一点一点地去吃透里面的技术细节并且还需要不断地总结和提炼否则对知识的理解可能就不够透彻浮于表面。
另外你需要多去动手实践特别是和算法相关的部分。比如在编译器的语法分析阶段有个内容是计算First集合和Follow集合其实理解起来并不困难但它包含的细节不少容易算错所以需要在课后多去练习。
我就是在课下把宫老师布置的习题和龙书上相关的题目都算了一遍还写了计算First集合和Follow集合的程序。不过就算是做到了这些我感觉对这部分的理解还不够透彻但是我对编译技术的恐惧感已经消除了对后续进一步的深入挖掘也打下了基础。所以说静下心来学勤动手去练对学习编译原理这门课程来说非常重要。
我还有一个体会就是学习编译原理没有捷径可走。因为编译技术是一个复杂而精密的工程它的知识是环环相扣的。比如说如果你对词法分析和语法分析的知识掌握得不够牢固不熟悉一些常用语法规则的推导过程那后面的IR、三地址代码、CFG等你就会学得一头雾水。
所以,我们只能够一步一个脚印地去学习。
当然了如果你和我一样是一个有家有口的上班族只利用碎片时间可能不好做到连续学习那我建议你在学习课程的时候可以稍微花点时间去参考一下宫老师介绍的开源编译器代码比如JavaCompiler的源码、Graal的源码。这样就可以和课程中的内容结合起来。因为看代码和调试代码会更加直观也更容易理解。
同时,你也可以看看别人的代码设计,有哪些地方是可以做成组件的,哪些函数是可以自己去实现来练手的。然后,你可以先给自己定一个小目标,就是利用业余时间去完成这些组件或者函数的开发,因为单个组件的工作量并不是太大,因此还是可以尝试完成的。这样在巩固理论知识的同时,还能锻炼自己的动手能力,我想热爱编程的你,应该可以从中得到不少快乐。
我相信,只要最终坚持下来,你和我,都可以掌握好编译这门“屠龙”技术。

View File

@ -0,0 +1,451 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
知识地图 一起来复习编译技术核心概念与算法
你好,我是学习委员朱英达。
在“预备知识篇”这个模块,宫老师系统地梳理了编译过程中各个阶段的核心要点,目的就是让我们建立一个编译原理的基础知识体系。那到今天为止,我们就学完了这部分内容,迈出了编译之旅中扎实的第一步。不知道你对这些知识掌握得怎样了?
为了复习,也为了检测我们的学习成果,我根据自己的知识积累和学习情况,整理了一张知识大地图,你可以根据这张地图中标记的七大编译阶段,随时速查常用的编译原理概念和关键算法。
如果你也总结了知识地图,那你可以对照着我这个,给自己一个反馈,看看它们之间有哪些异同点,我们可以在留言区中一起交流和讨论。
不过知识地图的形式,虽然便于你保存、携带、速查,但考虑到图中涉及的概念等内容较多,不方便查看和检索。所以,我还把地图上的知识点,用文字的形式帮你梳理出来了。你可以对照着它,来复习和回顾编译技术的核心概念和算法的知识点,构建自己的知识框架。
你在学习这些预备知识的过程中,可能会发现,宫老师并没有非常深入地讲解编译原理的具体概念、理论和算法。所以,如果你想继续深入学习这些基础知识,可以根据宫老师在每讲最后给出的参考资料,去学习龙书、虎书、鲸书等经典编译原理书籍。当然,你也可以去看看宫老师的第一季专栏课《编译原理之美》。
在我看来,相较于编译方面的教科书而言,《编译原理之美》这门课的优势在于,更加通俗易懂、与时俱进,既可以作为新手的起步指导,也能够帮助已经熟悉编译技术的工程师扩展视野,我很推荐你去学习这门课。所以,我邀请编辑添加了相应的知识点到《编译原理之美》的文章链接,如果你有深入学习的需要,你会很方便地找到它。
好了,一起开始复习吧!
一、词法分析根据词法规则把字符串转换为Token
核心概念:正则文法
正则文法:词法分析工作的主要文法,它涉及的重要概念是正则表达式。
正则表达式:正则文法的一种直观描述,它是按照指定模式匹配字符串的一种字符组合。
正则表达式工具字符串的模式匹配工具。大多数程序语言都内置了正则表达式的匹配方法也可以借助一些编译工具自动化根据正则表达式生成字符串匹配程序例如C++的Lex/Yacc以及Java的ANTLR。
具体实现:手工构造词法分析器、自动生成词法分析器
手工构造词法分析器
构造词法分析器使用的计算模型有限自动机FSA。它是用于识别正则文法的一种程序实现方案。
其组成的词法单元是Token也就是指程序中标记出来的单词和标点符号它可以分成关键字、标识符、字面量、操作符号等多个种类。
在实际的编译器中,词法分析器一般都是手写的。
自动生成词法分析器
具体实现思路把一个正则表达式翻译成NFA然后把NFA转换成DFA。
DFA确定的有限自动机。它的特点是该状态机在任何一个状态基于输入的字符都能做一个确定的状态转换。
NFA不确定的有限自动机。它的特点是该状态机中存在某些状态针对某些输入不能做一个确定的转换。这里可以细分成两种情况一种是对于一个输入它有两个状态可以转换另一种是存在ε转换的情况也就是没有任何字符输入的情况下NFA也可以从一个状态迁移到另一个状态。
技术难点
首先你需要注意NFA和DFA都有各自的优缺点以及不同的适用场景。
NFA优点是在设计上更简单直观缺点是它无法避免回溯问题在某些极端的情况下可能会造成编译器运行的性能低下。主要适用于状态较为简单且不存在回溯的场景。
DFA优点是它可以避免回溯问题运行性能较高缺点是DFA通常不容易直接设计出来需要通过一系列方案基于NFA的转换而得到并且需要占用额外的空间。主要适用于状态较为复杂或者对时间复杂度要求较为严苛的工业级词法分析器。
其次你需要了解基于正则表达式构造NFA再去进行模式匹配的算法思路。
从正则表达式到NFA这是自动生成词法分析器的一种算法思路。它的翻译方法是匹配一个字符i —>匹配“或”模式s|t —> 匹配“与”模式st —> 重复模式,如“?”“*”和“+”等符号它们的意思是可以重复0次、0到多次、1到多次注意在转换时要增加额外的状态和边。
从NFA到DFANFA的运行可能导致大量的回溯所以我们可以把NFA转换成DFA让字符串的匹配过程更简单。从NFA转换成DFA的算法是子集构造法具体的算法思路你可以参考第16讲。
二、语法分析:依据语法规则,编写语法分析程序,把 Token 串转化成 AST
核心概念:上下文无关文法
上下文无关的意思:在任何情况下,文法的推导规则都是一样的。
语法规则由4个部分组成一个有穷的非终结符或变元的集合、一个有穷的终结符的集合、一个有穷的产生式集合、一个起始非终结符变元。符合这四个特点的文法规则就是上下文无关文法。
两种描述形式一种是巴科斯范式BNF另一种是巴科斯范式的一种扩展形式EBNF它更利于自动化生成语法分析器。其中产生式、终结符、非终结符、开始符号是巴科斯范式的基本组成要素。
上下文无关文法与正则文法的区别:上下文无关文法允许递归调用,而正则文法不允许。上下文无关文法比正则文法的表达能力更强,正则文法是上下文无关文法的一个子集。
具体实现:自顶向下、自底向上
一种是自顶向下的算法思路它是指从根节点逐层往下分解形成最后的AST。
递归下降算法它的算法思路是按照语法规则去匹配Token串。优点程序结构基本上是跟文法规则同构的。缺点会造成左递归和回溯问题。注意递归下降是深度优先DFS只有最左边的子树都生成完了才会往右生成它的兄弟节点。
LL算法对于一些比较复杂的语法规则来说这个算法可以自动计算出选择不同产生式的依据。方法从左到右地消化掉 Token。要点计算 First 和 Follow 集合。
另一种是自底向上的算法思路它是指从底下先拼凑出AST的一些局部拼图并逐步组装成一棵完整的AST。
自底向上的语法分析思路移进把token加入工作区规约在工作区内组装AST的片段。
LR算法和 LL 算法一样,也是从左到右地消化掉 Token。
技术难点
首先你需要掌握LL算法的要点也就是计算First和Follow集合。
其次你要了解LL算法与LR算法的异同点。
LL算法优点是较为直观、容易实现缺点是在一些情况下不得不处理左递归消除和提取左因子问题。
LR算法优点是不怕左递归缺点是缺少完整的上下文信息编译错误显示不友好。
三、语义分析:检查程序是否符合语义规则,并为后续的编译工作收集语义信息
核心概念:上下文相关文法
属性文法上下文相关文法对EBNF进行了扩充在上下文无关的推导过程中辅助性解决一些上下文相关的问题。
注意上下文相关文法没有像状态图、BNF那样直观的分析范式。
应用场景:控制流检查、闭包分析、引用消解等。
场景案例
1.控制流检查
像return、break 和continue等语句都与程序的控制流有关它们必须符合控制流方面的规则。在 Java 这样的语言中,语义规则会规定:如果返回值不是 void那么在退出函数体之前一定要执行一个 return 语句,那么就要检查所有的控制流分支,是否都以 return 语句结尾。
2.闭包分析
很多语言都支持闭包。而要正确地使用闭包,就必须在编译期知道哪些变量是自由变量。这里的自由变量是指在本函数外面定义的变量,但被这个函数中的代码所使用。这样,在运行期,编译器就会用特殊的内存管理机制来管理这些变量。所以,对闭包的分析,也是上下文敏感的。
具体实现:引用消解、符号表、类型系统、属性计算
引用消解
概念解释:引用消解是一种非常重要的上下文相关的语义规则,它其实就是从符号表里查找被引用的符号的定义。
作用域:指计算机语言中变量、函数、类等起作用的范围。对于变量来说,为了找到正确的引用,就需要用到作用域。一般来说,它有两种使用场景,一种是标识符作用域,一种是词法作用域。
符号表
符号表内包含的信息:名称、分类、类型、作用域等。
存储形式:线性表格、层次化表格。
符号表的作用维护程序中定义的标识符ID类Token提供给编译器的各个环节进行操作。
建立符号表的过程整个编译器前端都会涉及到词法分析阶段将ID类Token收集到符号表中语法分析阶段可进行读取和补充语义分析阶段做引用消解时符号表的作用至关重要。
注意:符号表跟编译过程的多个阶段都相关。
类型系统
类型:在计算机语言里,类型是数据的一个属性,它的作用是来告诉编译器或解释器,程序可以如何使用这些数据。
类型系统:类型系统是一门语言所有的类型的集合,操作这些类型的规则,以及类型之间怎么相互作用的(比如一个类型能否转换成另一个类型)。
类型检查:这是与类型有关的分析和处理工作之一。主要用于对源程序代码中的一些类型不匹配的情况进行隐式类型转换或直接抛错。
子类型:面向对象编程时,我们可以给某个类创建不同的子类,实现一些个性化的功能;写程序时,我们可以站在抽象度更高的层次上,不去管具体的差异。把这里的结论抽象成一般意义上的类型理论,就是子类型。
类型转换比如说表达式“a=b+10”如果 a 的类型是浮点型,而右边传过来的是整型,那么一般就要进行缺省的类型转换。
参数化类型/泛型:泛型是程序设计语言的一种风格或范式。泛型允许程序员在强类型程序设计语言中编写代码时使用一些以后才指定的类型,在实例化时作为参数指明这些类型。
属性计算
编译器一般会采用属性计算的方法,来计算出每个 AST 节点的类型属性,然后检查它们是否匹配。
属性文法属性计算的方法就是基于语法规则来定义一些属性计算的规则在做语法解析或遍历AST的时候执行这些规则我们就可以计算出属性值。这种基于语法规则定义的计算规则被叫做属性文法Attribute Grammar
属性计算S属性综合属性、I属性继承属性
形成的数据结构Annotated AST带有标注信息的AST
语法制导的翻译:属性计算的特点是会基于语法规则,增加一些与语义处理有关的规则,我们把这种语义规则的定义叫做语法制导的定义,如果变成计算动作,就叫做语法制导的翻译。
四、运行时机制:程序的两种不同的执行模式
通常情况下,程序有两种执行模式:基于物理机、基于虚拟机。
在物理机上运行
举例C、C++、Golang。
程序运行的原理:基于指令指针寄存器的控制,顺序从内存读取指令执行。
CPU运行指令的地方。
多种架构x86、ARM、MIPS、RISC-V、PowerPC等。
关键构成:寄存器、高速缓存、功能单元。
汇编代码:操作码(让 CPU 执行的动作)、操作数(指令的操作对象,可以是常数、寄存器和某个内存地址)。
内存:执行指令相关的另一个硬件。
1.代码区:存放编译完成以后的机器码。-
2.静态数据区:保存程序中全局的变量和常量。-
3.栈:适合保存生存期比较短的数据,比如函数和方法里的本地变量。
重要知识点:栈帧的构成、活动记录、逐级调用过程。
栈的特点:申请和释放—修改栈顶指针,生存期与作用域相同。
4.堆:适合管理生存期较长的一些数据,这些数据在退出作用域以后也不会消失。
重要知识点通过操作系统API手动申请和释放。
管理机制:自动管理、手动管理。
操作系统:除了硬件支撑,程序的运行还需要软件,这就是运行时系统。
定义:除了硬件支撑,程序的运行还需要软件,这些软件叫做运行时系统。
操作系统:对于把源代码编译成机器码在操作系统上运行的语言来说(比如 C、C++),操作系统本身就可以看做是它们的运行时系统。
管理CPU资源分时执行。比如时间片轮转算法将CPU以时钟周期为单元执行多进程任务实现并发。
管理内存资源逻辑内存系统内核对内存地址的划定、物理内存硬件中具体每一个bit实际的硬件存储情况、虚拟内存基于操作系统内核对内存管理问题的抽象通常有一部分虚拟内存实际上是存在磁盘上的
在虚拟机上运行
举例Java、Python、Erlang、Lua。
程序运行的原理:虚拟机是计算机语言的另一种运行时系统。虚拟机上运行的是中间代码,而不是 CPU 可以直接认识的指令。
基于栈的虚拟机指令在操作数栈的栈顶获取操作数如JVM、Python虚拟机
优点:易于生成代码。
缺点:代码数量较多、不能充分利用寄存器。
基于寄存器的虚拟机类似于物理机从寄存器取操作数如Erlang、Lua、Dalvik、Ignition
优点与缺点:与栈机相反。
二者的区别:主要在于如何获取指令的操作数。
五、中间代码:运行各种优化算法、代码生成算法的基础
在这门课程中宫老师主要从用途和层次、解释执行、呈现格式和数据结构等角度来给你讲解IR这一关键概念。如果你想要更深入地了解IR的特点理解如何生成IR来实现静态编译的语言你可以去看《编译原理之美》的第24、25、26讲。
IR的用途和层次从抽象层次的角度来划分
第一类用途基于源语言做一些分析和变换HIR
第二类用途独立于源语言和CPU架构做分析和优化MIR
第三类用途依赖于CPU架构做优化和代码生成LIR
IR的解释执行
P-code直接用于解释执行的IR。由于它与具体机器无关因此可以很容易地运行在多种电脑上。这类IR对编译器来说就是做编译的目标代码。
注意P-code也可能被进一步编译形成可以直接执行的机器码。如Java的字节码。
IR的呈现格式
大部分IR没有像源代码和汇编代码那样的书写格式。
大多数的IR跟AST一样只是编译过程中的一个数据结构而已或者说只有内存格式。比如LLVM的IR在内存里是一些对象和接口。
为了调试的需要你可以把IR以文本的方式输出用于显示和分析。
IR的数据结构
第一种类似TAC的线性结构。
第二种:树结构。
第三种DAG-有向无环图。
第四种PDG-程序依赖图。
SSA格式的IR
概念SSA即静态单赋值。这是IR的一种设计范式它要求一个变量只能被赋值一次。
要点使用SSA的形式体现了精确的“使用-定义Use-def”关系并且由于变量的值定义出来以后就不再变化使得基于SSA更容易运行一些优化算法。
注意现代语言用于优化的IR很多都是基于SSA的包括Java的JIT编译器、JavaScript的V8编译器、Go语言的gc编译器、Julia编译器以及LLVM工具等。
六、代码分析与优化:优化程序对计算机资源的使用,以提高程序的性能
优化分类
是否与机器有关
机器无关:指与硬件特征无关,比如把常数值在编译期计算出来(常数折叠)。
机器有关:需要利用某硬件特有的特征,比如 SIMD 指令可以在一条指令里完成多个数据的计算。
优化范围
本地优化/局部优化:基本块内。
全局优化:函数(过程)内。
过程间优化:跨函数(过程)。
优化方法
1.把常量提前计算出来
常数折叠程序里的有些表达式肯定能计算出一个常数值那就不要等到运行时再去计算干脆在编译期就计算出来。比如“x=2*3”可以优化成“x=6” 。
常数传播:如果你一旦知道 x 的值其实是一个常量,那你就可以把所有用到 x 的地方,替换成这个常量。
稀疏有条件的常数传播:基于常数传播,还可以进一步导致分支判断条件转化为常量,导致一个分支的代码不会被执行。
2.用低代价的方法做计算
代数简化利用代数运算规则所做的简化比如“x=x*0”可以简化成“x=0”。
3.消除重复的计算
拷贝传播:遇到相同引用的变量,拷贝替换为同一个,节省内存到寄存器的操作,以此提升运算速度。
值编号VN和GVN把相同的值在系统里给一个相同的编号并且只计算一次。
公共子表达式消除CSE也会减少程序的计算次数。比如“x:=a+b”和“y:=a+b”x和y右边的形式是一样的就可以让y等于x从而减少了一次对“a+b”的计算。
部分冗余消除PRE是公共子表达式消除的一种特殊情况。
4.化零为整,向量计算
超字级并行SLP把基本块中的多个变量组成一个向量用一个指令完成多个变量的计算。
循环向量化:在循环展开的基础上,把多次计算优化成一个向量计算。
5.化整为零,各个优化
聚合体的标量替换SROA很多语言都有结构和对象这样的复合数据类型内部包含了多个成员变量这种数据类型叫做聚合体aggregates
编译器可以把聚合体的成员变量放在寄存器中进行计算,不需要访问内存。
6.针对循环,重点优化
归纳变量优化:归纳变量是指在循环体内由循环变量派生出来的变量,其变化是很有规律的,因此可以尝试做强度折减优化。
边界检查消除:在循环体内每次循环都会执行的边界检查代码,将其整合抽离出来,避免每次重复判断。
循环展开:通过把循环次数减少,但在每一次循环里,完成原来多次循环的工作量。
循环向量化:在循环展开的基础上,把多次计算优化成一个向量计算。
重组:在循环结构中,使用代数简化和重组,能获得更大的收益。
循环不变代码外提LICM在循环结构中如果发现有些代码其实跟循环无关那就应该提到循环外面去避免一次次重复计算。
代码提升:在条件语句中,如果多个分支条件里都执行了同一句代码,可将其提升至判断条件之前;如果是在循环体内,还可以继续借助循环不变代码外提优化,进一步提升到循环体之外,从而降低计算量。
7.减少过程调用的开销
尾调用优化和尾递归优化:尾调用就是一个函数的最后一句,是对另一个函数的调用。如果函数最后一句调用的函数是自己,就称为尾递归。尾调用可以将函数调用栈合并,尾递归还可以转换成循环,从而进一步做一系列针对循环语句的优化工作。
内联:内联也叫做过程集成,就是把被调用函数的代码拷贝到调用者中,从而避免函数调用。
内联扩展:内联扩展跟普通内联类似,也是在调用的地方展开代码。不过内联扩展被展开的代码,通常是手写的、高度优化的汇编代码。
叶子程序优化:叶子程序,是指不会再调用其他程序的函数(或过程)。因此,它也可以对栈的使用做一些优化。
8.对控制流做优化
不可达代码的消除:根据控制流的分析,发现有些代码是不可能到达的,可以直接删掉,比如 return 语句后面的代码。
死代码删除:通过对流程的分析,发现某个变量赋值了以后,后面根本没有再用到这个变量。这样的代码就是死代码,就可以删除。
if简化在讲常量传播时我们就见到过如果有可能if条件肯定为真或者假那么就可以消除掉 if 结构中的then块、else块甚至整个消除if结构。
循环简化:也就是把空循环或者简单的循环,变成直线代码,从而增加了其他优化的机会,比如指令的流水线化。
循环反转:这是对循环语句常做的一种优化,就是把一个 while 循环改成一个 repeat…until 循环(或者 do…while 循环)。这样会使基本块的结构更简化,从而更有利于其他优化。
拉直:如果发现两个基本块是线性连接的,那可以把它们合并,从而增加优化机会。
反分支:也就是减少程序分支,因为分支会导致程序从一个基本块跳到另一个基本块,这样就不容易做优化。比如,把循环内部的 if 分支挪到循环外面去,先做 if 判断,然后再执行循环,这样总的执行 if 判断的次数就会减少,并且循环体里面的基本块不那么零碎,就更加容易优化。
分析方法
控制流分析CFA 基于程序的控制语句(如条件语句、循环语句、分支语句和基本块语句等)进行分析,建立对程序执行过程的理解,从而进一步做出优化。
数据流分析DFA基于数据流分析框架包含“方向(D)”“值(V)”“转换函数(F)”“初始值(I)”和“交运算(Λ)”5 个元素)等方式,建立对程序中数据变化情况的理解,从而进一步做出优化。
依赖分析分析出程序代码的控制依赖Control Dependency和数据依赖Data Dependency关系。这对指令排序和缓存优化很重要。
别名分析:在 C、C++ 等可以使用指针的语言中,同一个内存地址可能会有多个别名,因为不同的指针都可能指向同一个地址。编译器需要知道不同变量是否是别名关系,以便决定能否做某些优化。
优化方法的重要性和顺序
重要性
对所有语言都重要:循环优化等。
面向对象语言:内联、逃逸等。
函数式语言:尾递归优化等。
顺序
要点:机器无关-早期,机器相关-后期。
注意:一个优化会导致另一个优化,同一个优化会多遍运行。
七、目标代码生成:编译器最后一个阶段的工作,生成针对不同架构的目标代码,也就是生成汇编代码
生成针对不同架构的目标代码
x86CISC指令Register-Memory架构。在指令里可以混合使用内存地址和寄存器。
ARMRISC指令Load-Store架构。在ARM的汇编中从寄存器到内存要使用str也就是Store指令而从内存到寄存器要使用ldr也就是Load指令。在这种架构下指令被分为内存访问Load和Store和ALU操作两大类且后者只能在寄存器上操作。
策略编写“代码生成器的生成器”。也就是把CPU架构的各种信息描述出来基于这些信息生成目标代码的生成器就像根据语法规则用ANTLR、bison这样的工具来生成语法解析器一样。
生成目标代码时的优化工作
1.指令选择
做指令选择的原因:生成更精简、性能更高的代码;使得同一个功能可以由多种方式实现。
算法树覆盖算法、自底向上的重写系统BURS
2.寄存器分配
原理:两个变量,不同时活跃,可以共享寄存器。
算法:图染色算法(优点-更优化,缺点-速度慢)、线性扫描算法(优点-不够优化,缺点-速度快)
3.指令排序
原理CPU内部的单元可并行。
实现基于数据依赖图的List Scheduling算法。
4.窥孔优化
思路提供一个固定大小的窗口比如能够容纳10条指令并检查窗口内的指令看看是否可以优化然后往下滑动窗口再次检查优化机会。
调用约定的影响
调用约定你可以发明自己的调用约定比如哪些寄存器用来放参数、哪些用来放返回值。但是如果要使用别的语言编译好的目标文件或者想让自己的编译器生成的目标文件被别人使用那就要遵守某种特定的ABI标准。
Unix和Linux系统的调用约定System V AMD64 ABI。
ABI即Application Binary Interface应用程序的二进制接口。
整体的处理过程
典型过程:指令选择->指令排序->寄存器分配->指令排序->Emit目标代码
要点基于LIR并且不断lower。
知识地图

View File

@ -0,0 +1,111 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
结束语 实战是唯一标准!
你好,我是宫文学。
转眼之间,“编译原理实战课”计划中的内容已经发布完毕了。在这季课程中,你的感受如何?有了哪些收获?遇到了哪些困难?
很多同学可能会觉得这一季的课程比上一季的“编译原理之美”要难一些。不过为什么一定要推出这么一门课,来研究实际编译器的实现呢?这是因为我相信,实战是检验你是否掌握了编译原理的唯一标准,也是学习编译原理的真正目标。
计算机领域的工程性很强。这决定了我们学习编译原理,不仅仅是掌握理论,而是要把它付诸实践。在我们学习编译原理的过程中,如果遇到内心有疑惑的地方,那不妨把实战作为决策的标准。
这些疑惑可能包括很多,比如:
词法分析和语法分析工具,应该手写,还是用工具生成?
应该用LL算法还是LR算法
后端应该用工具,还是自己手写?
我是否应该学习后端?
IR应该用什么数据结构
寄存器分配采用什么算法比较好?
……
上述问题,如果想在教科书里找到答案,哪怕是“读万卷书”,也是比较难的。而换一个思路,“行万里路”,那就很容易了。你会发现每种语言,因为其适用的领域和设计的目标不同,对于上述问题会采用不同的技术策略,而每种技术策略都有它的道理。从中,你不仅仅可以为上述问题找到答案,更重要的是学会权衡不同技术方案的考虑因素,从而对知识点活学活用起来。
我们说实战是标准。那你可能会反问,难道掌握基础理论和原理就不重要了吗?这是在很多领域都在争论的一个话题:理论重要,还是实践重要。
理论重要,还是实践重要?
理论和原理当然重要,在编译原理中也是如此。形式语言有关的理论,以及前端、中端和后端的各个经典算法,构筑了编译原理这门课坚实的理论基础。
但是,在出现编译原理这门课之前,在出现龙书虎书之前,工程师们已经在写编译器了。
你在工作中,有时候就会遇到理论派和实践派之争。举例来说,有时候从理论角度,某一个方案会“看上去很美”。那到底是否采用该方案呢?这个时候,就需要拿实践来说话了。
我拿Linux内核的发展举个例子。当年Linus推出Linux内核的时候并没有采用学术界推崇的微内核架构为此Linus还跟Minix的作者有一场著名的辩论。而实践证明Linux内核发展得很成功而GNU的另一个采用微架构的内核Hurd发展了20多年还没落地。
客观地说Linux内核后来也吸收了很多微内核的设计理念。而声称采用微内核架构的Windows系统和macOS系统其实在很多地方也已经违背了微内核的原则而具备Linux那样的单内核的特征。之所以有上述的融合其实都是一个原因就是为了得到更好的实用效果。所以实践会为很多历史上的争论划上句号。
在编译技术和计算机语言设计领域,也存在着很多的理论与实践之争。比如,理论上,似乎函数式编程更简洁、更强大,学术界也很偏爱它,但是纯函数的编程语言,至今没有成为主流,这是为什么呢?
再比如,是否一定要把龙书虎书都读明白,才算学会了编译原理呢?
再进一步,如果你使用编译技术的时候,遇到一个实际的问题,是跟着龙书、虎书还有各种课本走,还是拿出一个能解决问题的方案就行?
在课程里,我鼓励你抛弃一些传统上学习编译原理的困扰。如果龙书、虎书看不明白,那也不用过于纠结,这并不会挡住你学习的道路。多看实际的编译器,多自己动手实践,在这个过程中,你自然会明白课本里原来不知所云的知识点。
那么如何以实践为指导,从而具备更好的技术方案鉴别力呢?在本课程里,我们有三个重点。包括研究常用语言的编译器、从计算机语言设计的高度来理解编译原理,以及从运行时的实现来理解编译原理。
对于你所使用的语言,应该把它的编译器研究透
这门课程的主张是,你最好把自己所使用语言的编译器研究透。这个建议有几个理由。
第一因为这门语言是你所熟悉的所以你研究起来速度会更快。比如可以更快地写出测试用的程序。并且由于很多语言的编译器都已经实现了自举比如说Go语言和Java语言的编译器所以你可以更快地理解源代码以及对编译器本身做调试。
第二这门语言的编译器所采用的实现技术一定是体现了该语言的特性的。比如V8会强调解析速度快Java编译器要支持注解特性等值得你去仔细体会。
第三研究透编译器会加深你对这门语言的理解。比如说你了解清楚了Java的编译器是如何处理泛型的那你就会彻底理解Java泛型机制的优缺点。而C++的模板机制对于学习C++的同学是有一定挑战的。但一旦你把它在编译期的实现机制弄明白就会彻底掌握模板机制。我也计划在后续用一篇加餐把C++的模板机制给你拆解一下。
那么,既然编译器是为具体语言服务的,所以,我们也在课程里介绍了计算机语言设计所考虑的那些关键因素,以及它们对编译技术的影响。
从计算机语言设计的高度,去理解编译技术
在课程里你已经体会到了,语言设计的不同,必然导致采用编译技术不同。
其实,从计算机语言设计的高度上看,编译器只是实现计算机语言的一块底层基石。计算机语言设计本身有很多的研究课题,比如类型系统、所采用的编程范式、泛型特性、元编程特性等等,我们在课程里有所涉猎,但并没有在理论层面深挖。有些学校会从这个方向上来培养博士生,他们会在理论层面做更深入的研究。
什么样的计算机语言是一个好的设计?这是一个充满争议的话题,我们这门课程尽量不参与这个话题的讨论。我们的任务,是要知道当采用不同的语言设计时,应该如何运用编译技术来落地。特别是,要了解自己所使用的语言的实现机制。
如果说计算机语言设计,是一种偏理论性的视角,那么程序具体的运行机制,则是更加注重落地的一种视角。
从程序运行机制的角度,去理解编译技术
学习编译原理的一个挑战,就在于你必须真正理解程序是如何运行的,以及程序都可以有哪几种运行方式。这样,你才能理解如何生成服务于这种运行机制的目标代码。
最最基础的你需要了解像C语言这样的编译成机器码直接运行的语言它的运行机制是怎样的。代码放在哪里又是如何一步步被执行的。在执行过程中栈是怎么变化的。函数调用的过程中都发生了些什么事情。什么数据是放在栈里的什么数据是放在堆里的等等。
在此基础上如果从C语言换成C++呢C++多了个对象机制那对象在内存里是一个什么结构多重继承的时候是一个什么结构在存在多态的时候如何实现方法的正确绑定这些C++比C语言多出来的语义你也要能够在运行时机制中把它弄清楚。
再进一步到了Go语言仍然是编译成机器码运行的但跟C和C++又有本质区别。因为Go语言的运行时里包含了垃圾收集机制和并发调度机制这两个机制要跟你的程序编译成的代码互相配合所以编译器生成的目标代码里要体现内存管理和并发这两大机制。像Go语言这种特殊的运行机制还导致了跨语言调用的难度。用Go语言调用C语言的库要有一定的转换和开销。
然后呢语言运行时的抽象度进一步增加。到了Java语言就用到一个虚拟机。字节码也正式登台亮相。你需要知道栈机和寄存器机这两种不同的运行字节码的解释器也要知道它们对应的字节码的差别。而为了提升运行速度JIT、分层编译和逆优化机制又登场栈上替换OSR技术也产生。这个时候你需要理解解释执行和运行JIT生成的本地代码是如何无缝衔接的。这个时候的栈桢又有何不同。
然后是JavaScript的运行时机制就更加复杂了。V8不仅具备JVM的那些能力在编译时还要去推断变量的类型并且通过隐藏类的方式安排对象的内存布局以及通过内联缓存的技术去加快对象属性的访问速度。
这样从最简单的运行时,到最复杂的虚拟机,你都能理解其运行机制的话,你其实不仅知道在不同场景下如何使用编译技术,甚至可以参与虚拟机等底层软件的研发了。
不再是谈论,来参与实战吧!
今天,我们学习编译原理,目标不能放在考试考多少分上。中国的技术生态,使得我们已经能够孕育自己的编译器、自己的语言、自己的虚拟机。方舟编译器已经带了个头。我想,中国不会只有方舟编译器孤军奋战的!
就算是开发普通的应用软件,我们也要运用编译技术,让它们平台化,让中国制造的软件,具有更高的技术含量,颠覆世界对于“中国软件”的品牌认知。这样的颠覆,在手机、家电等制造业已经发生了,也应该轮到软件业了。
而经验告诉我们,一旦中国的厂商和工程师开始动起来,那么速度会是非常快的。编译技术并没有多么难。我相信,只要短短几年时间,中国软件界就会在这个领域崭露头角!
这就是我们这门课程的目的。不是为了学习而学习,而是为了实战而学习。
当然课程虽然看似结束了但也代表着你学习的重新开始。后面我计划再写几篇加餐会针对C++、Rust等编译器再做一些解析拓展你的学习地图。并且针对方舟编译器我还会进一步跟你分享我的一些研究成果希望我们可以形成一个持续不断地对编译器进行研究的社群让学习和研究不断深入下去不断走向实用。
另外我还给你准备了一份毕业问卷题目不多希望你能在问卷里聊一聊你对这门课的看法。欢迎你点击下面的图片用12分钟时间填写一下期待你畅所欲言。当然了如果你对课程内容还有什么问题也欢迎你在留言区继续提问我会持续回复你的留言。
我们江湖再见!