编译原理
基础
机器码和字节码有什么区别?
机器码(machine code),学名机器语言指令,有时也被称为原生码(Native Code),是电脑的CPU可直接解读的数据。
通常意义上来理解的话,机器码就是计算机可以直接执行,并且执行速度最快的代码。
字节码(Bytecode)是一种包含执行程序、由一序列 op 代码/数据对 组成的二进制文件。字节码是一种中间码,它比机器码更抽象,需要直译器转译后才能成为机器码的中间代码。
通常情况下它是已经经过编译,但与特定机器码无关。字节码通常不像源码一样可以让人阅读,而是编码后的数值常量、引用、指令等构成的序列。
字节码在运行时通过JVM(JAVA虚拟机)做一次转换生成机器指令,因此能够更好的跨平台运行。
字节码是一种中间状态(中间码)的二进制代码(文件)。需要直译器转译后才能成为机器码。
可以通过javascript进行对比:
机器码所占用的空间远远超过了字节码,所以使用字节码可以减少系统的内存使用。
参考
编程语言的runtime和compiler有什么区别?请以js为例说明下。
compiler也就是编译器是将代码编译为机器代码的工具。
我们说的的js引擎比如V8将js代码编译成了字节码,只针对少量场景使用其TurboFan模块编译为了优化过的机器码,所以其TurboFan可以勉强算作编译器。
关于字节码和机器代码的区别可以参考机器码和字节码有什么区别?
关于v8工作原理,可以参考简单说下v8引擎工作原理
汇编代码与机器代码是严格一一对应的,也很容易互相转换,这也是反编译的原理。
而runtime,是基于compiler之上的运行环境。比如浏览器端的document和node端的fs的提供者这种。
以require('asd.js')
为例,这句话是什么意思?v8是不关心的。
v8只关心你调了一个require函数,参数是个字符串,它只是执行这个函数;但是require这个函数到底能干嘛,是Node定义的。
所以在node架构图中:
下面两层都是runtime,而v8中的TurboFan最多算半个compiler。
参考:
对JIT的理解
即时编译(JIT)技术,是指字节码配合解释器和编译器。可以先了解v8引擎是如何工作的,简单来说,就是指解释器Ignition在解释执行字节码 的同时,收集代码信息,当它发现某一部分代码变热了之后,TurboFan编译器便闪亮登场,把热点的字节 码转换为机器码,并把转换后的机器码保存起来,以备下次使用。
对于JavaScript工作引擎,除了V8使用了“字节码+JIT”技术之外,苹果的SquirrelFish Extreme和Mozilla 的SpiderMonkey也都使用了该技术,Java和Python的虚拟机也都是基于这种 技术实现的。
用一张图来表示:
哈希是做什么?
哈希是一种通过对数据进行压缩, 从而提高效率的一种解决方法。
哈希存储的使用场景?
哈希表的哈希函数输入一个键,并向返回一个哈希表的索引。可能的键的集合很大,但是哈希函数值的集合只是表的大小。
哈希函数的其他用途包括密码系统、消息摘要系统、数字签名系统,为了使这些应用程序按预期工作,冲突的概率必须非常低,因此需要一个具有非常大的可能值集合的散列函数。
密码系统:给定用户密码,操作系统计算其散列,并将其与存储在文件中的该用户的散列进行比较。(不要让密码很容易被猜出散列到相同的值)。
消息摘要系统:给定重要消息,计算其散列,并将其与消息本身分开发布。希望检查消息有效性的读者也可以使用相同的算法计算其散列,并与发布的散列进行比较。(不要希望伪造消息很容易,仍然得到相同的散列)。
参考:
你还应该知道的哈希冲突解决策略 - vivo互联网技术 - OSCHINA - 中文开源技术交流社区
哈希的冲突是什么?
来看一个简单的实例吧,假设采用hash函数:H(K) = K mod M,插入这些值:217、701、19、30、145
H(K) = 217 % 7 = 0
H(K) = 701 % 7 = 1
H(K) = 19 % 7 = 2
H(K) = 30 % 7 = 2
H(K) = 145 % 7 = 5
上面实例很明显 19 和 30 就发生冲突了。
你还应该知道的哈希冲突解决策略 - vivo互联网技术 - OSCHINA - 中文开源技术交流社区
哈希存储的键冲突(散列碰撞)可以有哪些解决方案?
哈希是一种通过对数据进行压缩, 从而提高效率的一种解决方法,但由于哈希函数有限,数据增大等缘故,哈希冲突成为数据有效压缩的一个难题。
冲突解决技术可以分为两类:开散列方法( open hashing,也称为拉链法,separate chaining )和闭散列方法( closed hashing,也称为开地址方法,open addressing )。这两种方法的不同之处在于:开散列法把发生冲突的关键码存储在散列表主表之外,而闭散列法把发生冲突的关键码存储在表中另一个槽内。
下面介绍业内比较流行的hash冲突解决策略:
线性探测(Linear probing)
双重哈希(Double hashing)
随机散列(Random hashing)
分离链接(Separate chaining)
上面线性探测、双重哈希、随机散列都是闭散列法,而分离链接则是开散列法。
参考:
你还应该知道的哈希冲突解决策略 - vivo互联网技术 - OSCHINA - 中文开源技术交流社区
什么是DSL?
DSL 即「Domain Specific Language」,中文一般译为「领域特定语言」
一种为特定领域设计的,具有受限表达性的编程语言。
在一些专有领域的任务处理上其实不需要那么多语言特性,DSL 就是在这种矛盾中产生的破局方案,它是为了解决特定任务的语言工具,比如文档编写有 markdown,字符串匹配有 RegExp,任务控制有 make、gradle,数据查找有 SQL,Web 样式编码有 CSS 等等。它的本质其实和我们很多软件工程问题的解决思路一样,通过限定问题域边界,从而锁定复杂度,提高编程效率。
参考:
什么是语法噪音?
js的function、Java的new/class、lisp系的括号等等。
实际意义不大,却因为语法设计,必须要加上的关键字符串,就是一种噪音。
内部DSL和外部DSL的本质区别是什么?
本质区别在于外部DSL需要从解析器开始实现自己的编译工具,内部DSL与宿主语言共享编译与调试工具等基础设施。
首先需要了解什么是dsl?。
我们先来个简单的例子,比如表示2周前的时间:
解法一
new Date(Date.now() - 1000 * 60 * 60 * 24 * 7 * 2);
解法二
2 weeks ago
解法三
(2).weeks().ago();
解法一是符合通用编程思维的解答,但即使作为程序员的我们也无法一眼看出其含义。
解法二和解法三其实就是 DSL 的两种不同类型——外部 DSL 和内部 DSL,它们的直观性显然更高,但它却无法直接运行
解法二称之为外部 DSL ,它是一种独立的编程语言,需要从解析器开始实现自己的编译工具,实现成本较高。但它的语法的灵活性更高,更容易达到用户的表现力需求。
外部 DSL 的直接对应就是 GPPL,由于受限语法特性更少,一般不要求图灵完备,所以它实现难度会低于 GPPL。
GPPL 即 「General Purpose Programming Language」,又称通用编程语言,例如我们常用的 JavaScript,它们被设计用来解决通用编程问题。
前端常用的模板引擎如 mustache 以及 React、Vue 支持的 JSX 语法都属于外部 DSL
解法三我们称之为 内部 DSL(Embedded DSL or Internal DSL) ,它是建立在其它宿主语言之上(一般为 GPPL)的特殊 DSL,它与宿主语言共享编译与调试工具等基础设施,学习成本更低,也更容易被集成。他在语法上与宿主语言同源,但在运行时上需要做额外的封装。
你也可以将内部DSL视为针对特定任务的特殊接口封装风格,比如 jQuery 就可以认为是针对 DOM 操作的一种内部 DSL。
参考:
你设计DSL的话该考虑些什么,比如语法噪音?
首先了解什么是语法噪音?
我设计的话优先考虑要不要造dsl,其次考虑易用性,最后考虑性能。
具体要造DSL的话,再从各个细节入手:
1、级联方法、级联管道、级联属性
级联方法是内部 DSL 的最常用模式
级联方法等链式调用风格的核心在于调用不再设计特定返回值,而是直接返回下一个上下文(通常是自身),从而实现级联调用。
级联管道参考gulp的pipe,级联属性通过js的class的get。
在 DSL 风格中,无论是级联方法、级联管道还是级联属性,本质都是链式调用风格,链式调用的核心是上下文传递,所以每一次调用的返回实体是否符合用户的心智是 DSL 设计是否成功的重要依据。
2、嵌套函数
嵌套函数本质上是将在链式调用中需要处理的上下文切换隐含在了函数嵌套操作中,所以它在层级抽象场景是非常适用的。
另外,嵌套函数在 DSL 的应用类似解析树,因为其符合语法树生成思路,往往可直接映射转换为对应外部 DSL,比如 JSX。
嵌套函数并不是万金油,它天然不适合流程、时间等顺序敏感的场景。
3、对象字面量
业界很多 DSL 都类似于配置文件,例如 JSON、YAML等外部 DSL,它们在嵌套数据展现中有很强的表达力。
对象字面量的结构性较强,一般只用来做配置等数据抽象的场景,不适合用在过程抽象的场景。
4、动态代理
JavaScript 中也有了一个更强大的语言特性,就是 Proxy,它可以代理属性获取。它除了可以轻松模拟出 Ruby 沾沾自喜的 method_missing 特性外,也可以有很多其它动态代理能力,这些都是实现内部 DSL 的重要工具。
5、Lambda表达式
Lambda 表达式本质上是一种直观易读且延迟执行的逻辑表达能力,从而避免额外的解析工作,不过它强依托宿主的语言特性支持(匿名函数 + 箭头表示),并且也会引入一定的语法噪音。
6、自然语言抽象
通过自然语言抽象,内部 DSL 的优势在单元测试场景中被发挥的淋漓尽致。通过增加类似自然语言的辅助语法(动、名、介、副等),可以使得程序语句更直观易懂。
总结:
参考:
原理
编译器的基本工作流程
编译一般分为三个步骤:
1、词法分析(laxical Analysis)
词法分析的意思就是,将代码块切分成最小的单位。这些最小单位称为token。比如 var a = 2;可以切分成var,a,=,2。
2、语法分析(Syntactic Analysis)
将词法单元转换成一个有层级,代表程序语法结构的树,这就是我们经常说的AST,抽象语法树。 注意:词法分析跟语法分析不是完全独立的,而是交错运行的。也就是说,并不是等所有的token都生成之后,才用语法分析器来处理。一般都是每取得一个token,就开始用语法分析器来处理了。
AST可是所有编译器以及转换器的基础核心,我们常用的babel转码过程就是先将ES6的代码编成AST,然后转换成ES5的AST,最后由这个AST还原出ES5代码。
3、代码生成(Code Genaration)
最后一步就是将AST转成计算机可以识别的机器指令码。
参考: